docs(website): record slice 3, and the boot-order defect it created

MODULE_SYSTEM.md §2.7.2 gains the slice 3 write-up: why the id and version are
written out in MODULES (the offline no-op has to be a file read, not a fetch),
the four decisions, and the table of what the smoke proved against the real
v0.3.0 release on a fresh database.

Two sections are corrected rather than extended. §2.5's "declarative module set"
promise did not anticipate that the two install surfaces need a rule about who
wins — the declaration owns the volume, the row owns whether a module runs — and
decision 4's "resolution runs before the server starts" is true of the SCAN, not
of the process: it runs inside start(), which is what buys it the database.

MODULE_API.md §2.6 no longer says the fragments are replayed by ensureSchema().
They are, for every caller except the server, which scans the volume later than
it ensures the schema and so replays them itself. That was a live defect for an
afternoon: it announced itself only as the "no module scan in this process" skip
line, which is correct output for `npm run seed` and means the opposite in a
booting server, and on a database that already had the tables the module started
perfectly. Nothing in the contract moves, so MODULE_API_VERSION is unchanged.

BACKEND_DESIGN.md: the boot sequence in the tree, and MODULES beside the
module_source_hosts setting it installs through.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-12 07:57:21 -05:00
parent 245be5dc91
commit 102ea6db19
3 changed files with 136 additions and 5 deletions

View File

@@ -296,8 +296,10 @@ naming a UO file, and the last thing binding core to the module.
### 2.5 Lifecycle
```
(core schema + seed) → MODULES resolved onto the volume ← §2.7.2 decision 4
require(module) → register(ctx, api) → [routes mounted, app.js require returns]
↓ (server.js, after ensureSchema + seed)
↓ (server.js: schema fragments, then)
onBoot(ctx) → started
↓ (SIGINT/SIGTERM)
onShutdown()
@@ -365,8 +367,8 @@ the dispatch guard reads and the row the admin panel reads are moved together an
### 2.6 Schema fragments
`schema` is an idempotent `.sql` file replayed by the same `ensureSchema()` that replays core's,
immediately after it, statement by statement, split the same way. It is subject to the same rules
`schema` is an idempotent `.sql` file replayed immediately after core's own `schema.sql`, statement
by statement, split the same way. It is subject to the same rules
core's file already follows: `CREATE TABLE IF NOT EXISTS`, `ALTER TABLE … ADD COLUMN IF NOT EXISTS`,
no `--` inside a string literal, no `DROP`.
@@ -404,6 +406,18 @@ break seeding outright. The replay asks `isLoaded()` and logs the skip. That is
use of that predicate: everywhere else, reading the module list before `load()` still throws, because
a booting server quietly getting no module tables is precisely what §7.6 exists to prevent.
**And the server replays them itself, because it now scans the volume LATER than it ensures the
schema.** `ensureSchema()` carries the replay for every ordinary caller, but `server.js` passes
`replayModules: false` and calls `replayFragments()` of its own accord after requiring `app.js`. The
reason is `MODULES` (`MODULE_SYSTEM.md` §2.7.2 decision 4): resolving a declared module set has to
happen before the scan, it needs the host-allowlist setting to do it, and that setting does not exist
until the schema and the seed have run — so core's schema now precedes the scan, and a replay wired
to core's schema would run when there was nothing yet to replay. Nothing about the contract moves:
fragments still run after core's tables exist and before any `onBoot`. **This was a live defect for
the length of one afternoon**, and the shape of it is worth keeping: it announced itself only as the
skip line above, appearing in a *booting server's* log where it means the opposite of what it means
in `npm run seed`, and on a database whose tables already existed the module started perfectly.
**Table names are namespaced and collision-checked.** New tables must be prefixed `<id>_`. The
loader extracts every `CREATE TABLE IF NOT EXISTS <name>` from the fragment and rejects the module
if a name collides with a core table or with another module's — a wrong `DROP`-free fragment can