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:
@@ -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 1–6 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
|
||||
|
||||
Reference in New Issue
Block a user