From 8db52c3d14bf27f0922a04343fe73afa82239efa Mon Sep 17 00:00:00 2001 From: wtclaude Date: Mon, 10 Aug 2026 04:52:59 -0500 Subject: [PATCH] docs(website): settle the OpenAPI fragment merge and the curated UI kit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both approved by the org lead 2026-08-10. §6.1 moves from open question to decision A with the per-side obligations spelled out in a new §6.1a: modules ship a swagger-fragment.json with fully-qualified paths and namespaced schema keys, core merges started modules' fragments into /api/docs.json at request time and always wins a key collision, and swagger-output.json stays exactly what core's own routes generate. Co-Authored-By: Claude --- website/MODULE_API.md | 36 +++++++++++++++++++++++++++++++----- website/MODULE_SYSTEM.md | 2 ++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/website/MODULE_API.md b/website/MODULE_API.md index 23ffa35..c731e9d 100644 --- a/website/MODULE_API.md +++ b/website/MODULE_API.md @@ -251,6 +251,13 @@ them leaves an operator with orphaned data and no supported way to remove it. - Call `process.exit`, install signal handlers, or start a listener. - Write outside `ctx.paths.moduleRoot` and the upload directory. +### 2.8 The OpenAPI fragment + +Every module that registers routes ships `swagger-fragment.json` in its bundle root. Core merges the +fragments of started modules into `/api/docs.json`; the full reasoning and the collision rules are +§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. + --- ## Part 3 — The client contract @@ -347,7 +354,8 @@ no-op, because no core nav item carries a `feature` today. ### 3.4 `ui` — the shared component kit -**This is the largest addition Phase 1 makes to the plan, and it is not optional** (§6.2). The atlas +**This is the largest addition Phase 1 makes to the plan, and it is not optional** (§6.2; approved +2026-08-10). The atlas pages alone import five core modules that are not React and not the router: `PublicLayout`, `PageHeader`, `Loading` / `ErrorState` / `EmptyState`, and `useAsync`. Without a shared kit a module either reaches into core's tree (violating the zero-import rule) or ships its own copies, which @@ -504,7 +512,7 @@ diff the whole workstream is allowed. Four things the survey found that the design of record gets wrong or does not cover. The first needs a decision. -### 6.1 OpenAPI generation does not survive a dynamic loader — **open question** +### 6.1 OpenAPI generation does not survive a dynamic loader — **settled: fragment merge** `MODULE_SYSTEM.md` §1.12 treats `scripts/routeManifest.js` and `swagger/swagger.js` as the same problem, because both require `app.js` with no database. They are not the same problem. @@ -522,12 +530,30 @@ spec*. Three ways out: | Option | How | Cost | | --- | --- | --- | -| **A. Fragment merge** *(recommended)* | Module CI runs swagger-autogen against its own `server/index.js` and ships `swagger-fragment.json` in the bundle. Core deep-merges the fragments of started modules into `/api/docs.json` at request time. | one merge helper in core (~40 lines); the module owns its own spec, which matches "one repo, one bundle" | +| **A. Fragment merge** ✅ | Module CI runs swagger-autogen against its own `server/index.js` and ships `swagger-fragment.json` in the bundle. Core deep-merges the fragments of started modules into `/api/docs.json` at request time. | one merge helper in core (~40 lines); the module owns its own spec, which matches "one repo, one bundle" | | B. Glob the modules dir | Core's `swagger.js` adds `modules/*/server/index.js` to `routes` when present. | core's *committed* spec then depends on which modules the developer had checked out — a spec that differs per machine | | C. Hand-write module paths into core's spec | — | a second source of truth; drifts on the first module release | -A is the only one that keeps the spec correct on an operator's box, where core is a prebuilt image -and the module arrived afterwards. **This needs sign-off before Phase 2 item 2.** +**Decided 2026-08-10: A.** It is the only one that keeps the spec correct on an operator's box, +where core is a prebuilt image and the module arrived afterwards. The obligation this puts on a +module is §2.8; the one it puts on core is Phase 2 item 2. + +### 6.1a The obligation, on each side + +**Module:** a `swagger-fragment.json` in the bundle root, generated by its own CI with the same +`swagger-autogen` tooling pointed at its own entry point, carrying only `paths`, `tags` and +`components.schemas`. Its paths must be fully qualified (`/api/v1/public/atlas/creatures`), because +the module knows its own mount prefixes and core does not re-derive them. Its `components.schemas` +keys are namespaced (`UoAtlasCreature`, not `AtlasCreature`) so two modules cannot collide in the +merged spec. CI fails the module build if a route it registers has no path in its fragment — the +per-module form of "never ship a route that isn't in the spec". + +**Core:** `/api/docs.json` merges the fragments of **started** modules over its own committed spec at +request time (cached, invalidated on a module state change). Merge is shallow-per-section and +**core always wins a key collision** — a module cannot redefine a core path, tag or schema by +shipping one with the same name; the collision is logged and the module's version dropped. +`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. ### 6.2 The client contract is much larger than §2.1 says diff --git a/website/MODULE_SYSTEM.md b/website/MODULE_SYSTEM.md index d3d46ba..8e359c3 100644 --- a/website/MODULE_SYSTEM.md +++ b/website/MODULE_SYSTEM.md @@ -508,6 +508,8 @@ Conventional Commits, the AI-disclosure trailer, branches cut from an up-to-date | 3 | Core declares an extension slot on `/admin/users/:id`; all six URLs preserved | §1.9 | | 4 | Phase 1 spike targets `/api/v1/public/atlas/*` | §2.7 | | 4a | The contract lives in [`MODULE_API.md`](MODULE_API.md); it is normative where the two differ | §2.7 | +| 4b | Modules ship an OpenAPI **fragment**; core merges started modules' fragments into `/api/docs.json` | API §6.1 | +| 4c | Core exposes a **curated, closed** UI kit + request primitive on `window.__rg`, versioned by `MODULE_API_VERSION` | API §3.4 | | 5 | Install surfaces: admin panel and the Docker environment; never a build step | §2.5 | | 6 | One repo, one bundle — server and client halves version together | §2.3 | | 7 | Android app is a separate plan; core owes it `/api/v1/public/modules` | §2.5, §2.7 |