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

Merged
whitlocktech merged 1 commits from docs/module-schema-replay into main 2026-08-10 22:05:59 +00:00
Member

Docs half of Phase 2 PR 3. Code half: RunicGateway/website#130.

MODULE_SYSTEM.md §2.7 gains the PR 3 entry; MODULE_API.md §2.6 gains the four decisions the section did not previously answer.

The decision that shaped the code

A fragment is validated at load time and executed later — the split is whether a database is needed to know the answer. Everything §2.6 states about the SQL (namespaced tables, IF NOT EXISTS, no DROP) is knowable by reading the file, so breaking one of those rules costs the module its mount entirely (§4.4's left-hand column) rather than mounting and then 503ing with its tables half created. What is left for the replay is the class of failure only the server can report — an unknown column type, a bad foreign key — and that is post-mount and answers 503.

The other three

  • A leading-verb allowlist, not a DROP denylist. CREATE, ALTER, INSERT, UPDATE — the four core's own schema.sql uses. §2.6 words the rule as "no DROP", but the file is replayed on every boot: TRUNCATE and DELETE would empty a table at every restart and RENAME would fail at the second one. A denylist only ever bans what somebody thought of. The section is explicit that this is a leading-verb check and claims no more — ALTER TABLE x DROP COLUMN y passes it, and catching that needs a SQL parser.
  • The replay sits outside ensureSchema()'s retry loop. That loop exists to wait for the database; a fragment that throws is one module's failure, not a signal the database is not ready, and retrying core's whole schema nine more times over one module's bad SQL would turn a 503'd module into a two-minute boot.
  • Partial application is accepted, not compensated for. MariaDB self-commits each DDL statement, so no transaction could roll back the tables created before the failing one; §2.6's idempotence rule is what makes re-running a corrected fragment safe.

One thing found while wiring it

db/seed.js (npm run seed) calls ensureSchema() standalone without ever requiring app.js, so no scan has happened and fragments()'s §7.6 throw would have broken seeding outright. It is now the one sanctioned caller that replays nothing and says so in the log — everywhere else, reading the module list before load() still throws, because a booting server quietly getting no module tables is what §7.6 exists to prevent.


  • AI-assisted: written with Claude Code (Opus 5), reviewed before opening.
Docs half of Phase 2 PR 3. Code half: RunicGateway/website#130. `MODULE_SYSTEM.md` §2.7 gains the PR 3 entry; `MODULE_API.md` §2.6 gains the four decisions the section did not previously answer. ## The decision that shaped the code **A fragment is validated at load time and executed later** — the split is whether a database is needed to know the answer. Everything §2.6 states about the SQL (namespaced tables, `IF NOT EXISTS`, no `DROP`) is knowable by reading the file, so breaking one of those rules costs the module its mount entirely (§4.4's left-hand column) rather than mounting and then 503ing with its tables half created. What is left for the replay is the class of failure only the server can report — an unknown column type, a bad foreign key — and that is post-mount and answers 503. ## The other three - **A leading-verb allowlist, not a `DROP` denylist.** `CREATE`, `ALTER`, `INSERT`, `UPDATE` — the four core's own `schema.sql` uses. §2.6 words the rule as "no `DROP`", but the file is **replayed on every boot**: `TRUNCATE` and `DELETE` would empty a table at every restart and `RENAME` would fail at the second one. A denylist only ever bans what somebody thought of. The section is explicit that this is a leading-verb check and claims no more — `ALTER TABLE x DROP COLUMN y` passes it, and catching that needs a SQL parser. - **The replay sits outside `ensureSchema()`'s retry loop.** That loop exists to wait for the database; a fragment that throws is one module's failure, not a signal the database is not ready, and retrying core's whole schema nine more times over one module's bad SQL would turn a 503'd module into a two-minute boot. - **Partial application is accepted, not compensated for.** MariaDB self-commits each DDL statement, so no transaction could roll back the tables created before the failing one; §2.6's idempotence rule is what makes re-running a corrected fragment safe. ## One thing found while wiring it `db/seed.js` (`npm run seed`) calls `ensureSchema()` standalone without ever requiring `app.js`, so no scan has happened and `fragments()`'s §7.6 throw would have broken seeding outright. It is now the one sanctioned caller that replays nothing and says so in the log — everywhere else, reading the module list before `load()` still throws, because a booting server quietly getting no module tables is what §7.6 exists to prevent. --- - [x] AI-assisted: written with Claude Code (Opus 5), reviewed before opening.
wtclaude added 1 commit 2026-08-10 21:58:48 +00:00
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>
whitlocktech merged commit bcd3a750e7 into main 2026-08-10 22:05:59 +00:00
whitlocktech deleted branch docs/module-schema-replay 2026-08-10 22:06:00 +00:00
Sign in to join this conversation.
No description provided.