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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<rows>` | `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<string\|null>` | `model/settings` | `shardAtlas.model` |
|
||||
| `ctx.settings.set` | `(key, value) => Promise<void>` | `model/settings` | `shardAtlas.model` |
|
||||
| `ctx.settings.set` | `(key, value, updatedBy?) => Promise<void>` | `model/settings` | `shardAtlas.model:60` |
|
||||
| `ctx.settings.getInstanceName` | `() => Promise<string>` | `model/settings` | `shardIngest.js:84` |
|
||||
| `ctx.auth.getUserFromRequest` | `(req) => { id, username, role } \| null` | `utils/auth` | `shardVisibility.js` |
|
||||
| `ctx.push.publish` | `(streamId, { ref?, ownerUserId? }) => Promise<void>` | `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
|
||||
`<repo>/modules/<id>/`, 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 `<repo>/modules/<id>/`. 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 `<script type="importmap">` that would supply one. Each shared dependency needs a two-line
|
||||
alias shim that re-exports from `window.__rg`. `output.globals` does not help: it applies to
|
||||
iife/umd output only.
|
||||
|
||||
### 7.3 §6.1 confirmed empirically, not just predicted
|
||||
|
||||
Regenerating the OpenAPI spec after the move deleted **361 lines** — all six atlas paths — from
|
||||
`swagger-output.json`, with `Swagger-autogen: Success` and no warning of any kind. The route manifest
|
||||
kept all six in the same run. That is the static-analysis-versus-runtime split of §6.1 happening for
|
||||
real, and it is exactly the silent failure the fragment merge exists to prevent. Core's committed
|
||||
spec is correct as regenerated — it describes core's own routes — and the six paths come back via
|
||||
module-uo's fragment when Phase 2 item 2 lands.
|
||||
|
||||
### 7.4 The loader's failure guarantees are tested, not asserted
|
||||
|
||||
`server/test/moduleLoader.test.js` — 17 tests over the paths nobody exercises by hand: an entry point
|
||||
that throws, a `coreApi` mismatch, an unknown manifest key, an id that disagrees with its directory,
|
||||
two modules claiming one prefix, a module claiming a core prefix, registering an undeclared prefix
|
||||
and declaring an unregistered one, a fragment naming a core table, an unprefixed table, a schema with
|
||||
no purge, an `onBoot` that throws, an `onShutdown` that hangs and one that throws, a double
|
||||
registration, and a probe asserting `ctx` exposes exactly the documented surface and is frozen.
|
||||
|
||||
The property under test throughout is the same: **the failing module fails alone.**
|
||||
|
||||
### 7.5 Spike artifacts that are NOT design
|
||||
|
||||
Three things in the branch are consequences of stopping at six routes, and Phase 3 removes all three.
|
||||
They are recorded so nobody reads them as intended shape:
|
||||
|
||||
1. **Core reaches into the module twice** — `admin/shardAtlas.controller.js` and
|
||||
`test/atlasController.test.js` require the module's model directly. The five admin atlas routes
|
||||
live at `/admin/shard/atlas/*`, inside the `/shard` prefix core still owns, so the module cannot
|
||||
take them without colliding or moving a URL. Phase 3 moves the whole `/shard` admin prefix at once
|
||||
and the imports go with it.
|
||||
2. **Two copies of `shardVisibility`** — the module's (as `utils/visibility.js`) and core's, for the
|
||||
shard routes not yet extracted. Two five-second caches over the same table; functionally
|
||||
identical. Predicted in §6.3, observed exactly as described.
|
||||
3. **The module has no `swagger-fragment.json`** — §6.1a's obligation needs core's merge helper on
|
||||
the other side of it, which is Phase 2.
|
||||
|
||||
### 7.6 A finding for Phase 2's loader
|
||||
|
||||
`scan()` is lazy — requiring the loader does not run it. That is deliberate (app.js decides when
|
||||
modules are discovered) but it is a sharp edge: a caller that requires the loader and reads nothing
|
||||
gets an empty, *silent* module list. It cost one confusing test failure during the spike. Phase 2
|
||||
should either make the trigger explicit in the name or scan at require time and let app.js order the
|
||||
require.
|
||||
|
||||
@@ -392,6 +392,13 @@ hook. The spike must *also* prove the §2.6 chunk load end to end, since that is
|
||||
decision in the plan. Exit criteria: no internal-file imports, `npm run routes:manifest` produces a
|
||||
zero-line diff, and the chunk loads under the enforced CSP.
|
||||
|
||||
**Phase 1 is complete.** The spike ran on `website` branch `spike/module-atlas` (cut from `edge`,
|
||||
never merged) and **met all three exit criteria** — see [`MODULE_API.md`](MODULE_API.md) Part 7. §2.6
|
||||
survives intact: the prebuilt chunk loads and renders under `script-src 'self'` with zero violation
|
||||
reports. The one thing it changed is that §2.6's one-React rule turns out to have a server-side twin
|
||||
nobody had written down — a module cannot resolve core's `express` either, so core hands that over
|
||||
too (API §7.2).
|
||||
|
||||
**Phase 2 — Core scaffolding, no behaviour change.** One PR each, in order:
|
||||
|
||||
1. `installed_modules` table + the §2.4 state machine.
|
||||
|
||||
Reference in New Issue
Block a user