docs(website): settle where the module state machine lives and how boot treats it

Phase 2 PR 1 of the module system records two things 2.4 left open: the
states are stored in one `state` column rather than a policy flag beside a
runtime one, and every boot recomputes the outcome states while leaving
`disabled` alone.

That second rule is the one with consequences worth writing down -- a
startup_failed module is retried on every restart, so an operator who fixes
the cause needs no admin-panel visit; a running module can never display a
stale failure reason; and disabling, the one operator decision rather than
outcome, survives restarts. Also states what the row does NOT decide: the
loader scans the filesystem before the database is reachable, so the URL
surface is a property of the volume, which is what keeps
routes.manifest.json generatable against a dead database.

BACKEND_DESIGN.md 3 gains the installed_modules columns alongside the other
tables.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-10 06:35:37 -05:00
parent 27bfdc9152
commit 3510c2ecf1
2 changed files with 67 additions and 0 deletions

View File

@@ -665,6 +665,34 @@ JSON. `resolveMany()` returns only ids that resolved to something displayable
`~1_val~` are stripped, since the bridge sends the id and never the property packet that carries the
arguments — and it never throws, because a cliloc lookup is decoration on a character sheet.
### installed_modules — what is installed, and what happened to it (module system)
One row per installed module, keyed by the `id` from its `module.json` — the same id that names its
directory on the modules volume and its URL segment.
| Column | Shape |
|---|---|
| `id` | VARCHAR(32) PK — the module id |
| `name`, `version` | the manifest's label and semver, for the admin Modules screen |
| `state` | ENUM `installed` / `enabled` / `disabled` / `started` / `startup_failed` |
| `failure_stage`, `failure_reason` | the stage a failure happened at (`manifest`, `core_api`, `mounts`, `extensions`, `schema`, `require`, `register`, `boot`) and its recorded reason |
| `source`, `sha256` | the release the bundle came from and the digest verified before unpacking; both NULL for a directory placed on the volume by hand |
| `installed_at`, `started_at`, `updated_at` | `started_at` is the last **successful** start |
**This table never decides which routes exist.** The module loader scans the filesystem at require
time, before the database is reachable, so the URL surface is a property of the volume — which is what
lets `routes.manifest.json` be generated against a dead database. A disabled module stays mounted and
is guarded; the row decides whether it *answers*, not whether it is there.
**Every boot resets each non-disabled row to `enabled`** and clears its recorded failure, then the load
writes that boot's outcome. So a `startup_failed` module is retried on the next restart (an operator
who fixes the cause needs no admin-panel visit), a running module can never display a stale reason,
and `disabled` — the one operator *decision* rather than outcome — survives untouched. A re-install or
upgrade refreshes the metadata and leaves `state` alone.
Design of record: [`MODULE_SYSTEM.md`](MODULE_SYSTEM.md) §2.4; the loader's obligations are
[`MODULE_API.md`](MODULE_API.md) Part 4.
---
## 4. API contract