feat(release): ship an OpenAPI fragment, a frozen manifest and a bundle (phase 3, slice 5)
The three artifacts that make this module installable and checkable, closing
phase 3's extraction. Nothing about what the module serves changes: the same 72
URLs, the same behaviour.
**The OpenAPI fragment (MODULE_API.md §2.8, §6.1a) was never built, on either
side.** The 417 `#swagger` annotations came across in slice 1 and went nowhere,
and core's /api/docs.json merged nothing — so every route this module serves was
in no spec at all, which is core's standing rule ("never ship a route that isn't
in the spec") being broken by the extraction rather than by a route.
`server/scripts/swaggerFragment.js` generates it. The prefixes are DERIVED: the
script runs the module's own `register()` against a recording api and asks
`require.cache` which file each router came from, so a mount prefix exists in one
place — `server/index.js` — and not in a table beside it. The 31 schemas moved
here from core's swagger.js, namespaced `Uo…` because core wins every key
collision in the merge; `Error` and `ValidationError` stay referenced by core's
names, since they resolve in the merged document.
**The frozen route manifest (§5.3)** is derived too, and by subtraction: CI
clones core at the ref pinned in ci/core-ref.json, generates its manifest without
this module and then with it, and the difference is what this module serves. That
buys the half of §5.3 that matters most for free — a module that shadowed or
displaced one of core's routes shows up as a REMOVAL, not merely as an addition
elsewhere. The same job checks the fragment against ground truth: every route
must have an operation and every operation must be a route.
**The release workflow** publishes `module-uo-<version>.tar.gz` plus a manifest
carrying its sha256. The version is declared in module.json rather than computed
from commit subjects, and the workflow never writes to a branch — it tags and
publishes — so `main` needs no push exception. The bundle is assembled from an
include list, because an exclude list ships whatever it forgot.
Four annotation defects, inherited from core and never visible until something
generated a spec from these files: two `requestBody` literals a brace short (the
route documented with an empty body), and two descriptions whose inner quoting
swagger-autogen cannot survive — it re-quotes `"` and a backtick to `'` before
evaluating, so either inside a single-quoted description ends the string early
and the annotation is dropped. It reports each one and then prints Success in
green, so the generator now captures its diagnostics and makes them fatal.
Also fixed while writing it: passing one shared `doc` to swagger-autogen six
times. It renders components.schemas from an EXAMPLE object and writes the result
back into what it was handed, so each pass re-wrapped the last and the fragment
came out at 484 MB.
- 409 server tests (+21), 40 client tests unchanged
- swagger-fragment.json: 69 paths covering all 72 routes
- routes.manifest.json: 72 routes; core's own surface unchanged, 0 removals
- verified end to end by assembling the bundle exactly as CI will, unpacking it
into a real core and regenerating the manifest
Refs: docs/website/MODULE_SYSTEM.md §2.7.1, MODULE_API.md §2.8, §5.3, §6.1a
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
110
README.md
110
README.md
@@ -24,7 +24,7 @@ The module's **id** is `uo` — that is what appears in `module.json`, in the `i
|
||||
table, in the `modules/<id>/` path on disk and in the URL segment (`/uo/*`, `/admin/uo/*`,
|
||||
`/player/uo/*`). `Module-uo` is the repository; `module-uo` is the module and its release artifact.
|
||||
|
||||
## Status: the bundle skeleton exists; the extraction has started
|
||||
## Status: the extraction is complete; this repo is the UO half of the site
|
||||
|
||||
The design of record is
|
||||
[`website/MODULE_SYSTEM.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/website/MODULE_SYSTEM.md)
|
||||
@@ -37,32 +37,50 @@ in the docs repo — **read them before opening a PR here.** Where the two diffe
|
||||
| 0 — CI trigger fix, cut `website` `edge`, bootstrap this repo | `website`, here | ✅ done |
|
||||
| 1 — module API contract (`docs/website/MODULE_API.md`) + the atlas spike | `docs`, `website` | ✅ done |
|
||||
| 2 — core scaffolding: loader, `installed_modules`, registries, client registry | `website` | ✅ done |
|
||||
| 3 — extract the UO half of the site into this repo | `website`, here | 🟡 in progress |
|
||||
| 3 — extract the UO half of the site into this repo | `website`, here | ✅ done |
|
||||
| 4 — delivery: the admin Modules screen + the Docker path | `website` | ⬜ |
|
||||
|
||||
Phase 3 moves the UO half of `website/` here in ten slices (`MODULE_SYSTEM.md` §2.7.1), server-first
|
||||
and then client. Each slice is one PR here that adds, and one PR in `website` that deletes — this one
|
||||
merging first, so `website`'s `edge` branch serves the feature from core right up to the moment core
|
||||
drops it.
|
||||
Phase 3 moved the UO half of `website/` here in six slices (`MODULE_SYSTEM.md` §2.7.1): the bundle
|
||||
skeleton, the whole server half, core's client extension slots, the whole client half, the de-UO of
|
||||
core's own copy, and this one — the artifacts that make the result installable and checkable. Each
|
||||
slice was one PR here that added and one in `website` that deleted, this one merging first, so
|
||||
`website`'s `edge` branch served each feature from core right up to the moment core dropped it.
|
||||
|
||||
**Slice 0 is the bundle skeleton, and it registers nothing on purpose.** What it proves is the
|
||||
delivery path itself: core discovers the module, validates `module.json`, calls `register()`, serves
|
||||
the client chunk, injects it, and reports the module `started` — and the chunk resolves React, the
|
||||
renderer and the router from core's `window.__rg` rather than bundling its own. Every slice after
|
||||
this one adds registrations to `server/index.js` and `client/src/entry.jsx`.
|
||||
Neither half sliced by feature in the end, and for the same reason on both sides: a mount prefix is
|
||||
claimed whole and a shared leaf moves with its **last** consumer, so the closure of either half is
|
||||
the whole half.
|
||||
|
||||
**What core serves and what this repo serves is now a fact you can read**, not a claim: 72 URLs, in
|
||||
[`routes.manifest.json`](routes.manifest.json), derived by loading this module into a real core and
|
||||
diffing. Not one of core's own URLs moved — that is the promise `MODULE_SYSTEM.md` §1.2 makes to the
|
||||
shipped Android app and the Discord bot, and it is checked on every PR.
|
||||
|
||||
## Working on it
|
||||
|
||||
```bash
|
||||
npm ci --prefix server && npm test --prefix server && npm run check:imports --prefix server
|
||||
npm run check:swagger --prefix server # is swagger-fragment.json still current?
|
||||
npm ci --prefix client && npm test --prefix client && npm run build --prefix client
|
||||
npm run check:externals --prefix client # asks the BUILT chunk, so it runs after the build
|
||||
```
|
||||
|
||||
The two `check:*` scripts are the contract's acceptance criteria rather than this module's own tests:
|
||||
no import may escape the module root (`MODULE_API.md` §5.1), and no bare specifier may survive into
|
||||
the built chunk (§3.6). The matching failure — a shared dependency being *bundled* — fails the build
|
||||
itself, from a guard inside `vite.config.js`.
|
||||
The `check:*` scripts are the contract's acceptance criteria rather than this module's own tests: no
|
||||
import may escape the module root (`MODULE_API.md` §5.1), no bare specifier may survive into the
|
||||
built chunk (§3.6), and the OpenAPI fragment core merges must describe the routes registered today
|
||||
(§2.8). The matching build failure — a shared dependency being *bundled* — comes from a guard inside
|
||||
`vite.config.js`.
|
||||
|
||||
**Changed a route, or its `#swagger` annotations?** `npm run swagger --prefix server` regenerates
|
||||
`swagger-fragment.json`; commit it. Core cannot generate it — core is a prebuilt image and this
|
||||
module mounts through a call no static parser can follow — so the file this repo commits is the one
|
||||
an operator's `/api/docs` shows.
|
||||
|
||||
**Changed a mount prefix, or added a route?** `routes.manifest.json` is regenerated by the
|
||||
`frozen-manifest` CI job, which clones core at the ref pinned in [`ci/core-ref.json`](ci/core-ref.json),
|
||||
loads this module into it and takes the difference. To do it locally, check this repo out into that
|
||||
core as `modules/uo` (**copy it — a symlink is silently skipped by the loader**), run core's
|
||||
`npm run routes:manifest` with and without it, and hand both files to
|
||||
`server/scripts/frozenManifest.js`.
|
||||
|
||||
Running it against a real core means checking this repo out as `website/modules/uo`, building the
|
||||
client half, and booting core. The four-step browser smoke in `MODULE_API.md` §7.7 is the only thing
|
||||
@@ -72,24 +90,35 @@ neither has a shape a DOM-less test runner can see.
|
||||
## What it contains
|
||||
|
||||
One repo, one bundle: the server half and the client half live side by side and version together, so
|
||||
a route and the screen that calls it can never be mismatched. A ✅ is in the tree today.
|
||||
a route and the screen that calls it can never be mismatched.
|
||||
|
||||
```
|
||||
module.json ✅ id, version, coreApi range, mounts, extensions
|
||||
server/index.js ✅ the entry point — register(ctx, api), synchronous, no database
|
||||
server/scripts/ ✅ checkImports.js — the §5.1 boundary check
|
||||
server/test/ ✅ node --test, with a fake ctx standing in for core
|
||||
server/ routers, controllers, models, utils
|
||||
server/db/schema.sql idempotent fragment, replayed by core's ensureSchema()
|
||||
server/db/purge.sql destructive; only ever run by an explicit purge
|
||||
client/src/entry.jsx ✅ the chunk's entry — registers routes, nav, feature provider
|
||||
client/src/shim/ ✅ react, react-dom, react-router-dom, jsx-runtime, from window.__rg
|
||||
client/vite.config.js ✅ the library build, the aliases, the not-bundled guard
|
||||
client/src/ route components, nav registrations, feature provider
|
||||
client/dist/ ✅ PREBUILT ESM chunk, built by CI — never by an operator
|
||||
module.json id, version, coreApi range, mounts, extensions
|
||||
swagger-fragment.json generated · the OpenAPI core merges into /api/docs.json
|
||||
routes.manifest.json generated · the 72 URLs this module serves
|
||||
ci/core-ref.json the core commit the two above were proved against
|
||||
server/index.js the entry point — register(ctx, api), synchronous, no database
|
||||
server/router/ routers + controllers, one directory per tier
|
||||
server/model/ one directory per table family; nothing crosses the boundary
|
||||
server/utils/ sidecar client, visibility, ingest, town crier, cliloc, atlas
|
||||
server/config/ the push stream catalog
|
||||
server/db/schema.sql idempotent fragment, replayed by core's ensureSchema()
|
||||
server/db/purge.sql destructive; only ever run by an explicit purge
|
||||
server/scripts/ the three checks: imports, the fragment, the frozen manifest
|
||||
server/test/ node --test, with a fake ctx standing in for core
|
||||
client/src/entry.jsx the chunk's entry — registers routes, nav, slots, feature provider
|
||||
client/src/shim/ react, react-dom, react-router-dom, jsx-runtime, from window.__rg
|
||||
client/vite.config.js the library build, the aliases, the not-bundled guard
|
||||
client/dist/ PREBUILT ESM chunk, built by CI — never by an operator
|
||||
```
|
||||
|
||||
Release artifact: `module-uo-<version>.tar.gz`, plus a manifest carrying its `sha256`.
|
||||
**The three generated files are committed on purpose.** Two of them are what core reads instead of
|
||||
looking at this source — it never has it — and the third records which core they were proved against.
|
||||
A generated file nobody reviews is a generated file nobody notices going wrong, so each lands in a
|
||||
diff.
|
||||
|
||||
Release artifact: `module-uo-<version>.tar.gz`, plus `module-uo-<version>.json` carrying its
|
||||
`sha256`. See below.
|
||||
|
||||
## How it reaches an operator
|
||||
|
||||
@@ -103,6 +132,29 @@ The [installer](https://gitea.whitlocktech.com/RunicGateway/installer) is **not*
|
||||
It deploys the *shard* side — the plugin overlay and the uo-link sidecar — and never contacts the
|
||||
website. Module delivery is website-side only.
|
||||
|
||||
### Releases
|
||||
|
||||
A merge to `main` that leaves `module.json` at a version with no release yet publishes one. The
|
||||
version is **declared**, not computed from commit subjects: `module.json`'s version is what core
|
||||
records in `installed_modules` and shows on the admin screen, and it sits beside the `coreApi` range
|
||||
a bump usually has to be weighed against — two sources for one number is how they drift. Bumping it
|
||||
is an ordinary reviewed PR.
|
||||
|
||||
Each release carries:
|
||||
|
||||
| Asset | What it is |
|
||||
|---|---|
|
||||
| `module-uo-<version>.tar.gz` | the directory core expects at `modules/uo/` — already assembled, with the chunk built and `ws` installed |
|
||||
| `module-uo-<version>.json` | id, version, `coreApi`, the artifact's URL, size and **`sha256`** |
|
||||
| `SHA256SUMS` | the same hash, in the shape every other repo here publishes |
|
||||
|
||||
Releases are **unsigned**; the `sha256` is the trust anchor, and the website verifies it before
|
||||
unpacking. That is the model `installer`'s bundles already use, and a second trust model would be a
|
||||
second thing to get right.
|
||||
|
||||
The tarball is assembled from an **include** list, never an exclude list — an exclude list ships
|
||||
whatever it forgot. Tests, scripts, `client/src` and the dev dependencies are not in it.
|
||||
|
||||
## Compatibility
|
||||
|
||||
`module.json` declares a `coreApi` semver range, checked at boot against core's `MODULE_API_VERSION`.
|
||||
|
||||
Reference in New Issue
Block a user