docs(website): settle how module schema fragments are validated and replayed

Docs half of website PR 3 (phase 2). MODULE_SYSTEM.md 2.7 gains the PR 3 entry;
MODULE_API.md 2.6 gains the decisions the section did not previously answer.

The one that shapes the code: a fragment is VALIDATED at load time and EXECUTED
later, split on whether a database is needed to know the answer. Everything 2.6
states about the SQL is knowable by reading the file, so breaking one of those
rules costs a module its mount entirely rather than mounting and 503ing with its
tables half created. What is left for the replay is the class of failure only
the server can report, and that is post-mount and answers 503.

Also recorded: the rules are enforced as a leading-verb allowlist (CREATE,
ALTER, INSERT, UPDATE -- the four core's own schema.sql uses) rather than the
DROP denylist 2.6 words them as, because the file is replayed on every boot and
a denylist only ever bans what somebody thought of; the replay sits outside
ensureSchema's wait-for-the-database retry loop, so one module's bad SQL cannot
cost the boot two minutes; partial application is accepted, since MariaDB
self-commits DDL and no transaction could undo it; and `npm run seed` is the one
sanctioned caller that replays nothing, because it never requires app.js and so
has no scan to read.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-10 16:58:11 -05:00
parent 510d10b297
commit 63e6c2b5d1
2 changed files with 46 additions and 4 deletions

View File

@@ -453,7 +453,7 @@ too (API §7.2).
Exit criterion: `routes.manifest.json` diff is zero lines and every existing test passes. If Phase 2
changes one URL, it is wrong.
**Progress: PRs 1-2 done.**
**Progress: PRs 1-3 done.**
- **PR 1** — `installed_modules` and the state machine, with the stored shape and the boot rules
settled in §2.4 above.
@@ -466,10 +466,18 @@ changes one URL, it is wrong.
de-entanglement registries and the two lifecycle hooks throw `not available until phase 2 PR 4/5`
rather than no-op — an accepting stub would let a module believe it had registered something.
28 tests, all on the failure paths.
- **PR 3** — schema fragment replay. `ensureSchema()` replays each installed module's fragment after
core's, with the statement splitter extracted to `utils/sqlStatements.js` so both are split by the
same code. The decision that shaped it, recorded in [`MODULE_API.md`](MODULE_API.md) §2.6: the
fragment is **validated at load time and executed later**, split on whether a database is needed to
know the answer — a fragment breaking a stated rule never mounts, while a failure only the server
could report (a bad column type) is post-mount and 503s. The rules are enforced as a **leading-verb
allowlist** (`CREATE`, `ALTER`, `INSERT`, `UPDATE`) rather than the `DROP` denylist §2.6 words them
as, because the file is replayed on **every boot**. Found while wiring it: `npm run seed` calls
`ensureSchema()` without ever requiring `app.js`, so the replay has to tolerate an unscanned loader.
There is still no module on the volume, no schema replay and no boot wiring, so this changes nothing
an operator or a client can see: 842 tests pass and `routes.manifest.json` is unchanged at 229
routes.
There is still no module on the volume and no boot wiring, so this changes nothing an operator or a
client can see: 856 tests pass and `routes.manifest.json` is unchanged at 229 routes.
**Phase 3 — Extract `module-uo`.** Moves out of `website/`: the 8 model directories and their 25
tables; the nine UO `utils/` files plus `newsGump.js`; the 13 router/controller files;