docs(website): settle what core publishes about an installed module

Phase 2, PR 6 of MODULE_SYSTEM.md 2.7 — GET /api/v1/public/modules.

MODULE_API.md gains 2.9, the normative shape: four fields (id, name, version,
capabilities) in scan order, and what is deliberately absent. Only started
modules appear, so a disabled or failed one is absent exactly as 4.4 already
leaves its routes and nav; no state, failure_stage or failure_reason reaches an
anonymous caller; no client chunk URL, because htmlShell hands the browser the
tag; an empty array is a real answer, but the 7.6 guard is a 500 and never [].
Also records why it owns the /modules prefix rather than sitting in the
root-mounted site router, where the loader's collision probe could not see it.

6.7 amends MODULE_SYSTEM 2.6 step 4, which said the SPA reads the endpoint "to
learn what to load" — step 3 of the same list had already answered loading a
different way, and 3.1.3 is the normative version. The endpoint feature-detects.
2.6 step 4 now says so, and Part 6's intro no longer claims a fixed count of
amendments.

MODULE_SYSTEM.md 2.7 records PR 6 and restates the phase exit criterion
honestly: no EXISTING URL moves, and PR 6 is the single deliberate addition in
Phase 2. Status line updated — the design is in implementation, not unimplemented.

BACKEND_DESIGN.md documents the route in the /public table and the router in the
folder map, and the published api-route-inventory.json mirror is refreshed to
228 public routes (the prose count was stale at 226).

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-10 22:03:35 -05:00
parent e733ac0c9c
commit 6a39ef63c6
4 changed files with 111 additions and 17 deletions

View File

@@ -1,12 +1,13 @@
# The Module System — design of record
**Status:** approved design, not yet implemented. Every decision in Part 3 has been settled with the
org lead; Part 1 records what was verified against the working trees on 2026-08-10, including the
places the original draft was wrong.
**Status:** approved design, **in implementation** — Phase 2's core scaffolding is landing on the
website `edge` branch, PRs 16 of 9 done (§2.7 tracks what each settled). Every decision in Part 3
has been settled with the org lead; Part 1 records what was verified against the working trees on
2026-08-10, including the places the original draft was wrong.
**The normative contract is [`MODULE_API.md`](MODULE_API.md)** (Phase 1). This document decides what
the module system *is*; that one decides exactly what a module may call. Where the two differ, that
one wins — its Part 6 lists the four places it amends this document.
one wins — its Part 6 lists every place it amends this document.
**Goal.** Turn Runic Gateway from a UO/ServUO-specific platform into a game-agnostic one. The core
architecture is unchanged — sidecar → website → browser. What changes is that game-specific
@@ -445,8 +446,12 @@ builds nothing, production pulls a prebuilt image, and `script-src 'self'` forbi
(`utils/htmlShell.js:111`), so this is an extension of a working mechanism, not a new one. The tag
is `<script type="module" src="/modules/uo/entry.js">` — same-origin, so `'self'` passes with no
nonce and no inline.
4. **The SPA reads `/api/v1/public/modules`** to learn what to load, then registers routes, nav and
its feature provider through `window.__rg.registry`.
4. **The SPA reads `/api/v1/public/modules`** to feature-detect against what this backend is
serving. Registration happens when the injected chunk executes and calls `window.__rg.registry`
it is not gated on this call. *(Amended by [`MODULE_API.md`](MODULE_API.md) §6.7: this step
originally said the SPA reads the endpoint "to learn what to load", which step 3 above had
already answered a different way. Nothing waits on an API round trip to start loading. The
endpoint's shape is API §2.9.)*
Phase 1 prototypes exactly this before anything is committed to it (§2.7).
@@ -457,7 +462,7 @@ Phase 1 prototypes exactly this before anything is committed to it (§2.7).
Nothing else can be trusted until the first of these is done.
**Phase 1 — API contract + spike (blocking).** Merge this document. Write the contract at
[`docs/website/MODULE_API.md`](MODULE_API.md) — **done**; it amends this document in four places,
[`docs/website/MODULE_API.md`](MODULE_API.md) — **done**; the places it amends this document are
listed in its Part 6, one of which (OpenAPI generation, §6.1 there) needs a decision before Phase 2
starts. Then a throwaway spike on an unmerged branch moving **`/api/v1/public/atlas/*`** behind the
proposed surface — the smallest honest test: six routes, DB-backed, no sidecar, no SSE, one boot
@@ -480,18 +485,21 @@ too (API §7.2).
3. `ensureSchema()` extended to replay module fragments after core's.
4. The three de-entanglement registries (§1.8), with core still the only registrant.
5. Boot/shutdown hook dispatch in `server.js`, likewise.
6. `GET /api/v1/public/modules` — installed ids, versions and capabilities, shaped like the existing
branding/site-settings endpoint. The SPA needs it to know what to load; the Android plan consumes
the same endpoint.
6. `GET /api/v1/public/modules` — ids, names, versions and capabilities of the modules currently
**serving**, shaped like the existing branding/site-settings endpoints (anonymous, database-free,
not site-mode gated). The SPA and the Android plan both feature-detect against it; it is not what
loads a client chunk ([`MODULE_API.md`](MODULE_API.md) §2.9 and §6.7).
7. Client `src/modules/registry.js`, the `window.__rg` shared-dependency global, and the
`htmlShell` script injection — empty registry, no visible change.
8. `MOD_PATHS` → `roles`-derived (§1.4); the generic feature-provider seam (§1.5).
9. `docker-compose.yml` gains the `modules` volume.
Exit criterion: `routes.manifest.json` diff is zero lines and every existing test passes. If Phase 2
changes one URL, it is wrong.
Exit criterion: no **existing** URL moves and every existing test passes. If Phase 2 changes one URL,
it is wrong. PR 6 is the single deliberate exception in the phase and it *adds*: `routes.manifest.json`
gains exactly one line, `GET /api/v1/public/modules`, and nothing else in the file moves. Every other
PR in Phase 2 produces a zero-line diff.
**Progress: PRs 1-5 done.**
**Progress: PRs 1-6 done.**
- **PR 1** — `installed_modules` and the state machine, with the stored shape and the boot rules
settled in §2.4 above.
@@ -561,6 +569,30 @@ fixed enum because the leg set is whatever has been registered.
Still nothing on the volume: **900 tests pass**, `routes.manifest.json` is unchanged at 229 routes
and the OpenAPI spec regenerates byte-identical.
- **PR 6** — `GET /api/v1/public/modules`, the first module-system URL a client can see. Four
decisions, all recorded in [`MODULE_API.md`](MODULE_API.md) §2.9: **`started` modules only**, so a
disabled or failed module is absent exactly as its routes and nav already are, and no visitor is
told that something is broken; **no `state`, `failure_stage` or `failure_reason`** on the public
surface — those are the admin screen's, and the reason is an exception string from inside core;
**no `client` chunk URL**, because `htmlShell` hands the browser the tag rather than a URL to fetch
(which amends §2.6 step 4 above — see API §6.7); and **no `siteMode` gate and no database**, the
same class as `/public/status` and `/public/version`, so a client can still feature-detect while
the site is in maintenance.
It is a **capability router of its own** rather than a fifth singleton in `site.router.js`, and that
is the load-bearing part. The loader's prefix-collision probe reads the live tier stack and skips
root-mounted layers, because a `use('/', …)` matches every path — so a route declared inside the
root-mounted site router is invisible to it. Mounting `use('/modules', …)` is what makes "no module
may ever claim `/modules`" a rule the loader enforces rather than a convention a reviewer has to
remember.
**910 tests pass** (+9, every one of them on the boundary: what must *not* appear). The route
inventory goes 229 → 230 (228 public + 2 internal) and moves by exactly the one added route;
`routes.guards.json` records it with an empty `gates` list, which is itself the assertion that the
endpoint is ungated. The OpenAPI spec gains the operation and the `PublicModules`/`PublicModule`
schemas. The published mirror [`api-route-inventory.json`](./api-route-inventory.json) is refreshed
to match.
**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;
`scripts/importSpawnAtlas.js` and `db/spawnAtlas.art.json`; `usersShard.controller.js` **minus