From 037f4abad2876a3cef47a635a0e5ce169ffbe445 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Mon, 10 Aug 2026 05:30:05 -0500 Subject: [PATCH] docs(website): record what the atlas spike proved Part 7 of MODULE_API.md: the three exit criteria and their results, the two contract changes the spike forced (ctx.express/ctx.validator and window.__rg.jsxRuntime), the empirical confirmation of the OpenAPI split, the loader's tested failure guarantees, and the three artifacts in the branch that are consequences of stopping at six routes rather than intended shape. Phase 1 is complete: docs/website/MODULE_API.md is written and the spike met every exit criterion on website branch spike/module-atlas (cut from edge, never merged). Co-Authored-By: Claude --- website/MODULE_API.md | 113 ++++++++++++++++++++++++++++++++++++++- website/MODULE_SYSTEM.md | 7 +++ 2 files changed, 118 insertions(+), 2 deletions(-) diff --git a/website/MODULE_API.md b/website/MODULE_API.md index c731e9d..2bab8bd 100644 --- a/website/MODULE_API.md +++ b/website/MODULE_API.md @@ -1,6 +1,8 @@ # The Module API — the contract -**Status:** Phase 1 deliverable of [MODULE_SYSTEM.md](MODULE_SYSTEM.md). This document is the +**Status:** Phase 1 deliverable of [MODULE_SYSTEM.md](MODULE_SYSTEM.md), **validated by the atlas +spike** — Part 7 records what the spike proved, what it changed in this contract, and the three +artifacts in it that are not design. This document is the normative contract between the core website and an installed module. `MODULE_SYSTEM.md` decides *what* the module system is; this decides *exactly what a module may call, what it must provide, and what core promises not to break*. @@ -111,11 +113,13 @@ module-uo does not need is on the list. | Member | Signature | Backed by | First real caller | | --- | --- | --- | --- | +| `ctx.express` | the `express` namespace | core's `node_modules` | every module router (§7.2) | +| `ctx.validator` | the `express-validator` namespace | core's `node_modules` | `atlas.router.js` | | `ctx.db.query` | `(sql, params?) => Promise` | `utils/db` | every `*.db.js` | | `ctx.db.pool` | mariadb pool | `utils/db` | `shardAtlas.db.js` (streamed import) | | `ctx.log` | `(namespace) => { error, warn, info, debug }`, each `(msg, meta?)` | `utils/logger` | all nine UO utils | | `ctx.settings.get` | `(key) => Promise` | `model/settings` | `shardAtlas.model` | -| `ctx.settings.set` | `(key, value) => Promise` | `model/settings` | `shardAtlas.model` | +| `ctx.settings.set` | `(key, value, updatedBy?) => Promise` | `model/settings` | `shardAtlas.model:60` | | `ctx.settings.getInstanceName` | `() => Promise` | `model/settings` | `shardIngest.js:84` | | `ctx.auth.getUserFromRequest` | `(req) => { id, username, role } \| null` | `utils/auth` | `shardVisibility.js` | | `ctx.push.publish` | `(streamId, { ref?, ownerUserId? }) => Promise` | `utils/pushDispatch:92` | `shardIngest.js:22` | @@ -135,6 +139,12 @@ Three narrowings from `MODULE_SYSTEM.md` §2.1, all deliberate: registration/game-signup/app-links policy that is core's business. - **`ctx.posts` is four functions.** `create`/`update`/`remove` are the CMS, not a module's. +And one addition the spike forced: **`ctx.express` and `ctx.validator`**. A module lives at +`/modules//`, outside `server/`, so Node's resolver never reaches `server/node_modules` and +`require('express')` from a module simply fails — which is how this was found. Even where it +resolved, a second express in the process is a second `Router` prototype. Core owns one express, as +it owns one React (§7.2). + `ctx` is frozen (`Object.freeze`, one level deep) before it is handed over. That is a guard against accident, not against a hostile module — per `MODULE_SYSTEM.md` §2.2 the boundary is organisational, not a security boundary. @@ -289,12 +299,18 @@ window.__rg = { react, // the React namespace reactDom, // react-dom/client router, // react-router-dom namespace + jsxRuntime, // react/jsx-runtime — see below registry, // §3.3 ui, // §3.4 api, // §3.5 } ``` +`jsxRuntime` is not decoration. A module's bundler compiles every `.jsx` file to imports from +`react/jsx-runtime` under the modern automatic runtime, and those have to resolve to *core's* React +like every other import. Without it on the global a module would have to build with +`jsxRuntime: 'classic'`; with it, the module uses the default its tooling already assumes. + A module entry checks `window.__rg.version` against its own `coreApi` range and refuses to register on a mismatch, logging once — the client-side twin of §1.1, and the reason `version` is here at all. @@ -588,3 +604,96 @@ files. Two consequences: Neither changes a decision; both are corrected here rather than left to be tripped over when the extraction is counted against the plan. + +--- + +## Part 7 — What the spike proved + +The Phase 1 spike ran on `website` branch `spike/module-atlas`, cut from `edge` and **deliberately +never merged** — it is the evidence, not the implementation. Phase 2 rebuilds the loader properly. + +### 7.1 The exit criteria + +| Criterion | Result | +| --- | --- | +| No internal-file imports from the module into core | **pass** — the module's only non-builtin requires are `ctx.express` / `ctx.validator`; the built client chunk contains **zero** bare import specifiers | +| `npm run routes:manifest` produces a zero-line diff | **pass** — `routes.manifest.json` *and* `routes.guards.json` are byte-identical with the six atlas routes now served by the module | +| The chunk loads under the enforced CSP | **pass** — `/uo/atlas` and `/uo/atlas/:slug` render from `/modules/uo/entry.js` under `script-src 'self'`, with **zero** violation reports at `/api/csp-report` and a clean console | +| Everything still passes | **pass** — 729 core tests, 81 module tests | + +Also verified end to end against the real database: the schema fragment replayed after core's +(`schema ensured for module "uo"`), `onBoot` ran the atlas refresh, the module reached `started`, and +the six API URLs answered 200 unchanged at `/api/v1/public/atlas/*`. + +### 7.2 One express, one React — the same rule, twice + +The single biggest thing the spike changed. `MODULE_SYSTEM.md` §2.6 got the client half right — one +React, shared via a global — and said nothing about the server, where the identical problem exists +and bites harder: + +- A module lives at `/modules//`. Node's resolver walks *up* from there and never reaches + `server/node_modules`, so `require('express')` inside a module **fails outright**. This was the + first error the spike hit. +- Installing express into the module would fix resolution and break something worse: two `Router` + prototypes, two sets of `instanceof` checks — and it would mean the operator running `npm install` + in a module directory, which is the build the whole plan exists to avoid. + +Hence `ctx.express` and `ctx.validator`. The rule generalises: **anything shared between core and a +module is owned by core and handed over — never resolved by the module.** On the client that is +`react`, `react-dom/client`, `react-router-dom` and `react/jsx-runtime`; on the server it is +`express` and `express-validator`. + +Two mechanical traps inside that, both cheap once known and both silent otherwise: + +- **Vite's object-form `resolve.alias` does PREFIX matching.** A `react` key also rewrites + `react/jsx-runtime` into `src/shim/react.js/jsx-runtime`, a path that cannot exist. Use the array + form with anchored regexes (`/^react$/`). +- **`external` alone is not enough for an ESM library build.** Rollup then emits bare + `import 'react'`, which the browser cannot resolve without an import map — and CSP forbids the + inline `