feat(modules): merge module OpenAPI fragments into /api/docs.json (phase 3, slice 5) #141
Reference in New Issue
Block a user
No description provided.
Delete Branch "feature/module-openapi-merge"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Core's half of the slice that closes phase 3. Two things: the request-time fragment merge core has owed since phase 1, and the last of core's UO copy.
Pairs with Module-uo#6 (merge that first) and
docs#….The merge core owed
MODULE_API.md§6.1a: a module shipsswagger-fragment.json, core merges the fragments of started modules into/api/docs.jsonat request time. Neither half existed.swagger/mergeSpec.jsnamed the request-time caller in its own header and that caller was never written — so the 72 routes module-uo serves were in no OpenAPI spec at all. That is core's standing rule ("never ship a route that isn't in the spec") broken by the extraction rather than by a route, which is why it lands here rather than in phase 4.swagger/docsSpec.jsis the merge. Four things about it are deliberate:swagger-output.jsonstays core-only. It is generated on a developer's machine and committed, so it has to come out the same regardless of what they had checked out. A module arrives on a volume long after the image was built, and core never has its sources.require()d JSON module — one in-place merge would be permanent for the life of the process and cumulative across rebuilds, a module's paths surviving its own uninstall.UoShardStatus) and references core's shared ones (Error) by core's name. The first would collide and lose; the second resolves in the merged document, where both exist./api/docs.jsonshowed the merged set.docsSpecis cached on a new loader state version, so the repeated call costs a comparison.A fragment that is missing, unreadable or not JSON costs that module its paths and nothing else — §4.4's bargain, where one module's failure is never the site's.
The last of core's UO copy
Slice 4 deferred these, and §5.2's check reads code, not prose, so none of it was caught:
swagger/swagger.js— 578 lines describing routes core has not served since slice 1. They moved to module-uo, namespaced, and come back through the merge on an instance that installs it.info.descriptionsaid "a private Ultima Online shard".## Shard integration (uo-link)section, which is now## Modules.TOWNCRIER_DURATION_SECandUOLINK_*in the two.env.examples. Read by the module, never by core; documented in the module's README instead of half-copied here.Two dropped annotations, and why nobody knew
swagger-autogen reports an annotation it cannot parse and then prints
Successin green, having skipped it.npm run swaggernow captures its diagnostics and fails — which immediately foundPOST /api/v1/admin/invitesandPOST /api/v1/auth/invite/:token/acceptdocumented with an empty request body, both since the day they were written. (The same class turned up four more times in module-uo, whose annotations came from here.)Auditing the tag list for the four UO orphans also turned up the same defect in the other direction: five tags used by routes and never declared —
Admin · Email,Admin · Invites,Admin · Moderation,Admin · Pages,Auth · Me. Declared now.Verification
routes.manifest.jsonunchanged — 158 public + 2 internal;check:modulescleanswagger-output.json: 128 paths, 69 schemas, 0 orphan tags, 0 orphan schemasstarted):/api/docs.jsonserves 197 paths — 128 core + 69 module — with all four module tags, 31Uo…schemas, no dangling$refs, and/api/docsrenders the module's operations grouped under their own tags with zero console errorsAI-assisted contribution. Written with Claude Code (Claude Opus). Commits carry
Co-Authored-By: Claude <noreply@anthropic.com>.🤖 Generated with Claude Code
Core's half of the slice that closes phase 3. Two things: the request-time fragment merge core has owed since phase 1, and the last of core's UO copy. **The merge (MODULE_API.md §6.1a).** `swagger-output.json` is core's own routes and cannot be anything else — it is generated on a developer's machine and committed, so it must come out the same regardless of what they had checked out, and a module arrives on a volume long after the image was built. Module routes therefore reach the document at request time, from the `swagger-fragment.json` each module ships: `swagger/docsSpec.js` merges the fragments of STARTED modules over the committed spec, cached on a new loader state version and rebuilt when a module's state moves. Until now neither half existed. `swagger/mergeSpec.js` named the request-time caller in its header and that caller was never written, so the 72 routes module-uo serves were in no OpenAPI spec at all — core's standing rule ("never ship a route that isn't in the spec") broken by the extraction rather than by a route. Core wins every key collision, `swagger-output.json` is never mutated (it is a require()d JSON module — one in-place merge would be permanent AND cumulative), and a fragment that is missing or unreadable costs that module its paths and nothing else. The Swagger UI is now built per request for the same reason the JSON is: bound once at require time it would show core's routes for the life of the process while /api/docs.json showed the merged set. **The last of core's UO copy** (slice 4 deferred it; §5.2's check reads code, not prose, so none of this was caught): - 31 UO schemas and 4 UO tags in `swagger/swagger.js`, describing routes core has not served since slice 1 — 578 lines. They moved to module-uo, namespaced `Uo…`, and arrive back through the merge on an instance that installs it. - `info.description` said "a private Ultima Online shard". - README.md's 48 UO mentions, including the architecture diagram and the whole `## Shard integration (uo-link)` section, now `## Modules`. - `TOWNCRIER_DURATION_SEC` and `UOLINK_*` in the two `.env.example`s: read by the module, not by core, and documented in the module's README instead. **Two dropped annotations, and the reason nobody knew.** swagger-autogen reports an annotation it cannot parse and then prints Success in green, having skipped it. `npm run swagger` now captures its diagnostics and fails — which immediately found `POST /api/v1/admin/invites` and `POST /api/v1/auth/invite/:token/accept` documented with an EMPTY request body, both since the day they were written. Fixing the tag list also cleared five tags used by routes but never declared (`Admin · Email`, `Admin · Invites`, `Admin · Moderation`, `Admin · Pages`, `Auth · Me`) — the same defect class, in the other direction. - 646 server tests (+9), 157 client tests unchanged - routes.manifest.json unchanged (158 public + 2 internal); check:modules clean - swagger-output.json: 128 paths, 69 schemas, 0 orphan tags, 0 orphan schemas - verified against a real boot with module-uo installed: 197 merged paths (128 core + 69 module), all four module tags, 31 Uo schemas, no dangling $refs, /api/docs renders the module's operations with zero console errors Refs: docs/website/MODULE_API.md §2.8, §6.1a; MODULE_SYSTEM.md §2.7.1 Co-Authored-By: Claude <noreply@anthropic.com>