Skip to content

Commit e3aa111

Browse files
docs(spec): name only the OpenAPI document where the retired /docs viewer page was named (#25162)
<!-- claude-projects-attribution: {"github_login":"os-bill"} --> _Requested by **Bill** · [project thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wg6zwDuV2XvoDkqC7bXgqnpc)_ Fixes #25121 Clause-②: no Metadata-protocol: none ## What changed `main` no longer serves the OpenAPI document's HTML viewer page (`GET {basePath}/docs`, retired by PR #25124, `1b3b65e64`). `registerRoutes` gates `registerOpenApiEndpoints` on `api.enableOpenApi`, and that registrar now mounts one route, `GET {basePath}/openapi.json` (`packages/rest/src/discovery-routes.ts`, read at `origin/main` `23540582f`). The spec text that still named the viewer now names the OpenAPI document only: | Site | Before | After | |:---|:---|:---| | `rest-server.zod.ts`, `enableOpenApi` TSDoc | `openapi.json` plus "`GET {basePath}/docs` for a zero-dep Scalar-rendered HTML viewer" | the one endpoint, `GET {basePath}/openapi.json` | | `rest-server.zod.ts`, `enableOpenApi` describe | "Enable OpenAPI 3.1 spec & docs viewer endpoints" | "Enable the OpenAPI 3.1 document endpoint (`GET BASEPATH/openapi.json`)" | | `rest-server.zod.ts`, `documentation.enabled` tombstone prescription | "…leaves the `/openapi.json` document and its `/docs` viewer unmounted." | "…leaves the `/openapi.json` document unmounted." | | `liveness/rest_api.json`, `enableOpenApi` evidence | "mounts both `/openapi.json` and the `/docs` viewer" | "mounts `/openapi.json`, the OpenAPI document"; `verifiedAt` restamped 2026-10-22 (call graph re-closed against `origin/main`) | | `scripts/lib/openapi-self-consistency.ts`, rule 1 comment and the dangling-ref refusal | named "the Scalar viewer at GET /api/v1/docs" as a consumer | names the document at `GET /api/v1/openapi.json` and client generators | | `migrations/entries/semantic/18.rest-api-config-dead-keys-retired.ts`, `replacement` and `acceptanceCriteria` | "the OpenAPI document and the docs viewer", "`GET /openapi.json` and `GET /docs` unmounted" | the OpenAPI document only, "`GET /openapi.json` unmounted" | Regenerated: `gen:schema` (no tracked change), `gen:docs` → `content/docs/references/api/rest-server.mdx` (the two describe rows and the prescription row). One `patch` changeset for `@objectstack/spec`: `.changeset/25121-spec-openapi-viewer-prose.md`. The `documentation.enabled` refusal is an accept-set no-op: same key refused, same issue path, same opening sentence; only its last clause changed. The two pins that read the prescription (`packages/spec/src/api/rest-api-config-dead-keys-retirement.test.ts` `DOCS_ENABLED_PRESCRIPTION` and `packages/rest/src/rest-api-config-dead-keys-refused.test.ts`) match up to "`api.enableOpenApi: false` is the switch", which the new text keeps, so neither needed a flip. Both are green below. ## Tests (at `32bfe10cd`, the final head) - `pnpm --filter @objectstack/spec test` (lock VERDICT command-exit 0): Test Files 706 passed (706), Tests 21462 passed, 1 todo. - `pnpm --filter @objectstack/spec typecheck` (lock VERDICT command-exit 0). - `pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 src/rest-api-config-dead-keys-refused.test.ts src/rest-docs-viewer-retired.pin.test.ts` (lock VERDICT command-exit 0): Test Files 2 passed, Tests 17 passed. - `pnpm --filter @objectstack/spec check:generated`: exit 0, all 14 artifacts up to date (it was red on `check:docs` before `gen:docs`). - No ablation: no tested behaviour changed (description, TSDoc, comment and prescription text only). ## Gates `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `32bfe10cd` derived 111 commands; each was run with its exit code recorded before any pipe. `--ran` reconciliation: "111 derived famil(ies) accounted for — 110 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)". - 110 exit 0. Five first exited 3 (prerequisite not met: the lint/formula, client-react and objectql dists were unbuilt, and `check:exported-any` / `check:generated` had read a spec dist while a concurrent turbo build was rewriting it). They were re-run green after the builds. - NOT MEASURED: `pnpm check:dual-build-cjs-loads`, exit 3. It needs every workspace package built, and this worktree built only the spec, lint, formula, objectql and client-react closures. `Lint & Repo Gates` runs it in CI. - Local-only families: `check:skill-frame-freshness` exit 0. `check:dev-prereqs` exit 1, NOT MEASURED: it reports "the workspace is not built" (33 of 70 packages have no dist) and names no stale spec dist. That is the box's state, not a finding. - CI-owned families (the farm, Test Core, Build Core, Build Docs, type-check lanes): this PR's check-runs. ## Acceptance notes - Out of scope by the claim and untouched: `ApiTestingUiConfigSchema` (`ApiDocumentationConfigSchema.ui`, `documentation.zod.ts`, viewer enum including `'scalar'`) and `OpenApiGenerationConfigSchema.uiPath` (`plugin-rest-api.zod.ts`, default `/api/docs`, echoed by `REST_API_PLUGIN.md`). Measured at `origin/main` `23540582f`, `git grep -w` outside `packages/spec`, the generated references and the changelogs finds no reader of either. `ApiDocumentationConfig` appears once, in a comment at `packages/runtime/src/http-dispatcher.ts:3061`. `uiPath` appears once, as an unrelated local variable at `packages/rest/src/ui-routes.ts:79`. `RestApiPluginConfig` in `packages/rest` is its own interface (`rest-api-plugin.ts:94`), not the spec schema. The seat files these as their own card. - The changeset `.changeset/25077-rest-docs-viewer-attr-escape.md` (already on `main`) describes a viewer fix to a page that has since been retired. It is release-owned input, so it is not touched here. --- _Generated by [Claude Code](https://claude.ai/code/session_01B2EeooonSXrANpkpgUSfFZ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 425c743 commit e3aa111

6 files changed

Lines changed: 34 additions & 21 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
docs(spec): `api.enableOpenApi` and the `api.documentation.enabled` refusal name only the OpenAPI document; the retired docs viewer page is no longer described
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The REST server no longer serves an HTML viewer page at `GET {basePath}/docs`; it serves the machine-readable OpenAPI document at `GET {basePath}/openapi.json`, gated by `api.enableOpenApi`. The spec text now says only that:
11+
12+
- **`RestApiConfigSchema.enableOpenApi`.** The description read "Enable OpenAPI 3.1 spec & docs viewer endpoints", and the TSDoc named a Scalar-rendered viewer at `GET {basePath}/docs`. Both now name the one endpoint the flag mounts, `GET {basePath}/openapi.json`.
13+
- **The `api.documentation.enabled` refusal.** Its prescription said `api.enableOpenApi: false` leaves "the `/openapi.json` document and its `/docs` viewer unmounted". It now says the switch leaves the `/openapi.json` document unmounted. The key is refused exactly as before, at the same path, with the same opening sentence; only the closing words changed.
14+
- **The `rest-api-config-dead-keys-retired` upgrade entry** (shown by `os migrate meta` and the upgrade guide) drops the viewer from its replacement and acceptance text the same way.
15+
16+
No schema accepts or refuses anything new, no default changes, and no export moves.

‎content/docs/references/api/rest-server.mdx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ const result = BatchEndpointsConfigSchema.parse(data);
245245
| **enableUi** | `boolean` | optional (default: `true`) | Enable UI API endpoints (Views, Menus, Layouts) |
246246
| **enableBatch** | `boolean` | optional (default: `true`) | Enable batch operation endpoints |
247247
| **enableDiscovery** | `boolean` | optional (default: `true`) | Enable API discovery endpoint |
248-
| **enableOpenApi** | `boolean` | optional (default: `true`) | Enable OpenAPI 3.1 spec & docs viewer endpoints |
248+
| **enableOpenApi** | `boolean` | optional (default: `true`) | Enable the OpenAPI 3.1 document endpoint (`GET <basePath>/openapi.json`) |
249249
| **enableSearch** | `boolean` | optional (default: `true`) | Enable structured search endpoints (server-wide search opt-out; embedder-only, not settable from `os serve`) |
250250
| **enableProjectScoping** | `boolean` | optional (default: `false`) | Enable project-scoped routing for data/meta/AI APIs |
251251
| **projectResolution** | `Enum<'required' \| 'optional' \| 'auto'>` | optional (default: `"auto"`) | Project ID resolution strategy |
@@ -257,7 +257,7 @@ const result = BatchEndpointsConfigSchema.parse(data);
257257

258258
| Property | Type | Required | Description |
259259
| :--- | :--- | :--- | :--- |
260-
| **enabled** | `never` | optional | [REMOVED] `api.documentation.enabled` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — nothing ever read it: whether the server publishes its OpenAPI document is decided by the sibling `api.enableOpenApi` at the mount, so `enabled: false` turned nothing off. Delete the key; `api.enableOpenApi: false` is the switch that leaves the `/openapi.json` document and its `/docs` viewer unmounted. |
260+
| **enabled** | `never` | optional | [REMOVED] `api.documentation.enabled` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — nothing ever read it: whether the server publishes its OpenAPI document is decided by the sibling `api.enableOpenApi` at the mount, so `enabled: false` turned nothing off. Delete the key; `api.enableOpenApi: false` is the switch that leaves the `/openapi.json` document unmounted. |
261261
| **title** | `string` | optional | Title of the served OpenAPI document (`info.title`); unset keeps the bundled title |
262262
| **description** | `string` | optional | Description of the served OpenAPI document (`info.description`); unset keeps the bundled description. Your app's own release number belongs here |
263263
| **version** | `never` | optional | [REMOVED] `api.documentation.version` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — nothing ever read it, and the served OpenAPI document's `info.version` has one source: the protocol version, i.e. the version of the `@objectstack/spec` package that generated the document, which no deployment configuration overrides. Delete the key. To publish your app's own release number, write it into `api.documentation.description`, which the served `info.description` carries. |
@@ -293,7 +293,7 @@ const result = BatchEndpointsConfigSchema.parse(data);
293293
| **enableUi** | `boolean` | optional (default: `true`) | Enable UI API endpoints (Views, Menus, Layouts) |
294294
| **enableBatch** | `boolean` | optional (default: `true`) | Enable batch operation endpoints |
295295
| **enableDiscovery** | `boolean` | optional (default: `true`) | Enable API discovery endpoint |
296-
| **enableOpenApi** | `boolean` | optional (default: `true`) | Enable OpenAPI 3.1 spec & docs viewer endpoints |
296+
| **enableOpenApi** | `boolean` | optional (default: `true`) | Enable the OpenAPI 3.1 document endpoint (`GET <basePath>/openapi.json`) |
297297
| **enableSearch** | `boolean` | optional (default: `true`) | Enable structured search endpoints (server-wide search opt-out; embedder-only, not settable from `os serve`) |
298298
| **enableProjectScoping** | `boolean` | optional (default: `false`) | Enable project-scoped routing for data/meta/AI APIs |
299299
| **projectResolution** | `Enum<'required' \| 'optional' \| 'auto'>` | optional (default: `"auto"`) | Project ID resolution strategy |

‎packages/spec/liveness/rest_api.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,9 @@
6868
},
6969
"enableOpenApi": {
7070
"status": "live",
71-
"verifiedAt": "2026-09-21",
71+
"verifiedAt": "2026-10-22",
7272
"evidenceScope": "in-repo",
73-
"evidence": "packages/rest/src/rest-server.ts#registerRoutes (`if (this.config.api.enableOpenApi)` gates registerOpenApiEndpoints, which mounts both `/openapi.json` and the `/docs` viewer)",
73+
"evidence": "packages/rest/src/rest-server.ts#registerRoutes (`if (this.config.api.enableOpenApi)` gates registerOpenApiEndpoints, which mounts `/openapi.json`, the OpenAPI document)",
7474
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (parses `config.api` through `parseDeclaredApiConfig` and lists the key into `this.config.api`, the object every consumer below reads; the parsed schema's own `.default()`s supply the value when the host omits the key)",
7575
"note": "One consumer, and it is the mount. Worth reading beside `documentation.*` below: this flag decides WHETHER the OpenAPI document is served at all, and it is the only key on this block that the served document's existence depends on — nothing on this block decides what the document SAYS. REACHABILITY (#15543, standing reading): embedder-only. A RestServerConfig is the ARGUMENT a host passes when it constructs the server, and the one door is programmatic — createRestApiPlugin({ api }) (packages/rest/src/rest-api-plugin.ts). No shipped boot path opens it with a config of its own: packages/cli/src/commands/serve.ts forwards exactly two keys out of the stack's own top-level `api:` block (enableProjectScoping, projectResolution) and plugin-dev calls createRestApiPlugin() with no config at all, so a CLI-started deployment always gets the schema default for every key here. `live` answers who READS the key; reachability answers who can SET it, and the two never substitute for each other."
7676
},

‎packages/spec/scripts/lib/openapi-self-consistency.ts‎

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,10 @@
3030
*
3131
* 1. **Every local `$ref` resolves.** Any `$ref` beginning with `#/` is a JSON
3232
* Pointer into this same document; if it does not resolve, the document is
33-
* broken for every consumer that parses it (Scalar's viewer at
34-
* `GET /api/v1/docs`, and any client generator pointed at
35-
* `GET /api/v1/openapi.json`). Resolution is by pointer rather than by a
36-
* `#/components/schemas/` prefix match so that a future `#/$defs/…` ref
37-
* is covered without touching this file.
33+
* broken for every consumer that parses it (any client generator or
34+
* API tool pointed at `GET /api/v1/openapi.json`). Resolution is by
35+
* pointer rather than by a `#/components/schemas/` prefix match so that a
36+
* future `#/$defs/…` ref is covered without touching this file.
3837
* 2. **No schema is silently degraded.** See `assertNoDegradedSchemas`.
3938
*
4039
* ## Rule 1 after #5744 — vacuous today, retained for `$defs`
@@ -153,9 +152,9 @@ export function assertRefsResolve(doc: unknown): void {
153152
` defined components.schemas: [${defined.join(', ') || '<none>'}]\n` +
154153
`\n` +
155154
` A $ref that resolves to nothing breaks every consumer of the published\n` +
156-
` document (the Scalar viewer at GET /api/v1/docs renders an empty schema\n` +
157-
` panel; client generators fail at parse time). If components.schemas is\n` +
158-
` empty, the collector in build-openapi.ts skipped its inputs — note that\n` +
155+
` document at GET /api/v1/openapi.json (client generators fail at parse\n` +
156+
` time). If components.schemas is empty, the collector in\n` +
157+
` build-openapi.ts skipped its inputs — note that\n` +
159158
` lazySchema() returns a Proxy whose typeof is 'function', not 'object'\n` +
160159
` (#5168).`,
161160
);

‎packages/spec/src/api/rest-server.zod.ts‎

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -134,14 +134,13 @@ export const RestApiConfigSchema = lazySchema(() => z.object({
134134
enableDiscovery: z.boolean().default(true).describe('Enable API discovery endpoint'),
135135

136136
/**
137-
* Enable OpenAPI 3.1 spec endpoints — `GET <basePath>/openapi.json`
138-
* for the machine-readable contract and `GET <basePath>/docs` for a
139-
* zero-dep Scalar-rendered HTML viewer. The spec ships pre-generated
137+
* Enable the OpenAPI 3.1 document endpoint — `GET <basePath>/openapi.json`,
138+
* the machine-readable contract. The spec ships pre-generated
140139
* from @objectstack/spec and is enriched at request time with the
141140
* actual server URL + the runtime-registered objects (so `{object}`
142141
* placeholders expand into one path per concrete table).
143142
*/
144-
enableOpenApi: z.boolean().default(true).describe('Enable OpenAPI 3.1 spec & docs viewer endpoints'),
143+
enableOpenApi: z.boolean().default(true).describe('Enable the OpenAPI 3.1 document endpoint (`GET <basePath>/openapi.json`)'),
145144

146145
/**
147146
* Server-wide switch for the structured-search surface — and, like every key
@@ -232,8 +231,7 @@ export const RestApiConfigSchema = lazySchema(() => z.object({
232231
'`api.documentation.enabled` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — '
233232
+ 'nothing ever read it: whether the server publishes its OpenAPI document is decided by the sibling '
234233
+ '`api.enableOpenApi` at the mount, so `enabled: false` turned nothing off. Delete the key; '
235-
+ '`api.enableOpenApi: false` is the switch that leaves the `/openapi.json` document and its `/docs` '
236-
+ 'viewer unmounted.',
234+
+ '`api.enableOpenApi: false` is the switch that leaves the `/openapi.json` document unmounted.',
237235
),
238236
// [#20294] `.optional()`, no longer `.default('ObjectStack API')`: that
239237
// default was materialized into every present block and never served

‎packages/spec/src/migrations/entries/semantic/18.rest-api-config-dead-keys-retired.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const entry: SemanticMigration = {
1717
surface: 'restServer.api.responseFormat / restServer.api.documentation.enabled',
1818
replacement:
1919
'(removed — delete each key; neither had an effect to preserve. Whether the server publishes its '
20-
+ 'OpenAPI document and the docs viewer is `api.enableOpenApi`, the switch the mount already reads. '
20+
+ 'OpenAPI document is `api.enableOpenApi`, the switch the mount already reads. '
2121
+ 'Response shapes are fixed — each route answers in the response schema `@objectstack/spec/api` '
2222
+ 'declares for it — and are not a server-wide option, so there is no replacement for `responseFormat`.)',
2323
reason:
@@ -43,7 +43,7 @@ export const entry: SemanticMigration = {
4343
+ 'the REST plugin\'s `start`) with the retirement prescription, naming the key and '
4444
+ '`RestApiConfigSchema`, instead of being accepted and ignored; `tsc` refuses the key at the '
4545
+ 'authoring site (`never`). A host that meant "serve no OpenAPI document" sets `api.enableOpenApi: '
46-
+ 'false` and sees `GET /openapi.json` and `GET /docs` unmounted. Every client that parses REST '
46+
+ 'false` and sees `GET /openapi.json` unmounted. Every client that parses REST '
4747
+ 'responses reads each route\'s declared response shape. Every LIVE key of the `api` block — '
4848
+ 'including `documentation`\'s other members — parses byte-identically to before, and the mounted '
4949
+ 'REST surface is unchanged: '

0 commit comments

Comments
 (0)