docs(website): close phase 3 — slice 5, the fragment obligation, and the rust dry run
Records the slice that closes the extraction, and adds the two documents phase 3 owed: docs/modules/uo/ and the module-rust dry run. **The slice found an obligation neither repo had built.** MODULE_API.md §2.8 and §6.1a settle the OpenAPI fragment in detail — a module ships one, core merges the fragments of started modules into /api/docs.json. Neither half existed, so the 72 URLs module-uo serves were in no spec at all. §2.8 and §6.1a now record what was built, including the four things settled while building it: the filename is fixed rather than declared, a module namespaces what it DEFINES and references core's shared schemas by core's name, the generator derives its prefixes from the module's own register() call, and swagger-autogen's diagnostics have to be captured because it reports a broken annotation and then prints Success. **§5.3 gains the design decision the frozen manifest actually made:** it is a SUBTRACTION, not a prefix filter. Generating the manifest without the module and then with it answers "what does the module serve" AND "did core lose anything", and the second is the one §1.2 promises to the shipped Android app. A module that shadowed a core route cannot appear as an addition anywhere. **BACKEND_DESIGN.md §4.0.1** is new: /api/docs.json is assembled per request, the two generated artifacts are core's alone, and the route count was still 228. **docs/modules/** is new, per §2.10 (module documentation aggregates here, not in module repos): docs/modules/uo/README.md orients a reader on what module-uo serves, owns and needs from an operator, and links out to the feature docs that already existed rather than restating them. **docs/modules/rust-dryrun.md** is phase 3's fourth acceptance criterion. A written, deliberately unimplemented module for Rust — chosen because it wipes monthly, runs several servers rather than one shard, identifies by Steam, and ships RCON so there is no sidecar to write. The contract generalises: same manifest, same seven registration calls, same schema rules, and six of the UI kit's seven members wanted by a game with nothing in common with the one the kit was curated from. It found one real gap — **a module cannot register an identity provider**, and "Sign in with Steam" is what a Rust community expects. Recorded as the first candidate for a future MODULE_API_VERSION bump rather than bolted on: an identity provider participates in session creation, and §2.7's link-only SSO policy has to survive it. Also: website-README.md refreshed from the repo (it was several changes stale), and three settled decisions added (18-20). Pairs with Module-uo#6 and website#141. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -435,6 +435,33 @@ fragments of started modules into `/api/docs.json`; the full reasoning and the c
|
||||
§6.1a. In short: fully-qualified paths, namespaced schema keys, module CI fails if a registered
|
||||
route has no path in the fragment, and core wins every key collision.
|
||||
|
||||
**Built in phase 3 slice 5** (module-uo#6, website#141). Four things settled while building it, all
|
||||
of which a second module inherits:
|
||||
|
||||
- **The filename is fixed here, not declared in `module.json`.** `swagger-fragment.json` in the
|
||||
bundle root, like `module.json` itself — so a module cannot point core at some other file, and
|
||||
core's loader has one path to check. A module that ships none is simply absent from the merged
|
||||
document: whether it registered routes without documenting them is the *module's* CI to answer,
|
||||
where the routes are known. Core cannot tell a module with no routes from one that forgot.
|
||||
- **Namespace what you DEFINE; reference core's by core's name.** `UoShardStatus` is defined by the
|
||||
module; `#/components/schemas/Error` and `ValidationError` are referenced and **not** redefined.
|
||||
Both resolve in the merged document, which is the only place both halves exist — and shipping a
|
||||
copy of `Error` would be a collision core drops, arriving at the same result the expensive way.
|
||||
This is the practical form of "core wins": it makes the two cases feel different in the source,
|
||||
which is where the mistake would otherwise be invisible.
|
||||
- **Generate the fragment from the module's own registrations.** swagger-autogen needs a *file* and
|
||||
cannot follow `api.registerRoutes`, so the module's generator runs its own `register()` against a
|
||||
recording `api` and resolves each router back to its source through `require.cache`. A mount prefix
|
||||
then exists in exactly one place. The two values it cannot derive — the tier base paths and the
|
||||
slot's mount, both §2.4's — are checked against a real core by the §5.3 job rather than trusted.
|
||||
- **swagger-autogen reports a broken annotation and then succeeds.** It `console.error`s "Syntax
|
||||
error" or "out of structure", drops that annotation, and prints `Success`. Both repos' generators
|
||||
now capture those diagnostics and fail on them, which found six annotations documenting less than
|
||||
they claimed. Two ways one breaks: an object literal a brace short, and a `"` or backtick inside a
|
||||
single-quoted description (the tool re-quotes both to `'` before evaluating, ending the string
|
||||
early). A third, which nothing but a rendered page catches: an escaped apostrophe survives
|
||||
literally, because the annotation is not evaluated as JavaScript.
|
||||
|
||||
### 2.9 What core publishes about a module
|
||||
|
||||
`GET /api/v1/public/modules` — anonymous, database-free, never site-mode gated.
|
||||
@@ -1088,6 +1115,29 @@ and runs core's own `routeManifest.js`. Nothing else proves the URLs a module cl
|
||||
actually serves — a manifest frozen by hand goes stale silently, and the failure it would have caught
|
||||
is a route that moved.
|
||||
|
||||
**Built in slice 5, and what it does with that core is a SUBTRACTION.** The job generates the
|
||||
manifest without the module and then with it; the difference is what the module serves. Filtering the
|
||||
combined manifest by the module's prefixes would have answered only "what does the module serve".
|
||||
Subtracting also answers **"did core lose anything"** — and a module that shadowed or displaced a
|
||||
core route cannot appear as an addition anywhere, so that is the only way to see it. Three checks
|
||||
come out of one diff:
|
||||
|
||||
1. the added routes match the module's committed `routes.manifest.json`;
|
||||
2. **no route of core's was removed or changed**, which is `MODULE_SYSTEM.md` §1.2's promise;
|
||||
3. every added route has an operation in `swagger-fragment.json`, and every operation is an added
|
||||
route — §2.8's coverage requirement, answered against a running app rather than against a table.
|
||||
|
||||
The third is why this job matters beyond the manifest: everything else in a module's repo compares
|
||||
two strings that live in that repo. This compares a URL the module registers against a URL a real
|
||||
Express app reports serving, which is the only thing that can catch a fragment that is internally
|
||||
consistent and describes nothing.
|
||||
|
||||
Also learned here: **copy the module into the core checkout, never symlink it.** The loader filters
|
||||
its scan with `entry.isDirectory()`, which reports a link as a link and skips it silently — the
|
||||
manifest then comes out with no module routes and the diff looks like a module that registered
|
||||
nothing. And the client chunk must be **built before** the copy: `client.entry` is validated during
|
||||
the manifest step of the scan, so a missing chunk is a load failure, not a warning.
|
||||
|
||||
Pinning the ref rather than tracking `edge` is what keeps this from being a source of unexplained red
|
||||
Xes: core moves for reasons that have nothing to do with the module, and a bump is then a deliberate
|
||||
commit that says which core the module was last proved against.
|
||||
@@ -1142,6 +1192,25 @@ shipping one with the same name; the collision is logged and the module's versio
|
||||
`swagger-output.json` itself stays exactly what core's own routes generate, so `npm run swagger`
|
||||
remains reproducible on any machine regardless of what is installed.
|
||||
|
||||
Built as `server/swagger/docsSpec.js` (website#141). Three properties that are contract rather than
|
||||
implementation, because each one is a way the obvious version is wrong:
|
||||
|
||||
- **The committed spec is never mutated.** It is a `require()`d JSON module, so a merge in place
|
||||
would be permanent for the life of the process *and* cumulative across rebuilds — a module's paths
|
||||
outliving its own uninstall. Every rebuild starts from a structural copy.
|
||||
- **The Swagger UI is built per request too**, not bound once while `app.js` is still being required.
|
||||
Bound at require time it would show core's routes for the life of the process while
|
||||
`/api/docs.json` showed the merged set — two documents at two URLs, disagreeing.
|
||||
- **A bad fragment costs that module its paths and nothing else.** Missing, unreadable or not JSON is
|
||||
logged and skipped; the document still answers. That is §4.4's bargain — one module's failure is
|
||||
never the site's — and a docs page that 500s is strictly worse than one missing a module's routes.
|
||||
|
||||
`started` only, matching `clientEntryUrls()` rather than `clientChunks()`: the document is built when
|
||||
it is asked for, at which point the state is known, and documenting a module that 503s every one of
|
||||
those paths sends a client somewhere it cannot go. The cache key is a new
|
||||
`modules.version()` — a counter the loader bumps on every state *change*, which says nothing about
|
||||
which module moved or where to.
|
||||
|
||||
### 6.2 The client contract is much larger than §2.1 says
|
||||
|
||||
§2.1 lists three client registration calls and nothing else, implying React and the router are all a
|
||||
|
||||
Reference in New Issue
Block a user