Repository navigation
Commit 80153f5
feat(spec,rest)!: the served OpenAPI info carries the publisher's api.documentation identity; api.documentation.version retired (#20512)
Fixes #20294
Fixes #20359
Clause-②: yes (narrowing)
## Summary
Executes maintainer ruling **B** on #20359 (`5862477514`, batch #231
item 1, maintainer 「同意 B」) for the `rest-api-documentation` family of
the #18900 census, under ADR-0049 enforce-or-remove:
- **ENFORCED:** the eight identity members of
`RestServerConfig.api.documentation`. These are `title`, `description`,
`termsOfService`, `contact.name` / `contact.url` / `contact.email` and
`license.name` / `license.url`. An authored value now overlays the
served OpenAPI `info` on both doors, `{apiPath}/openapi.json` and the
environment-scoped twin. With nothing authored, the served `info` is
byte-identical to the artifact.
- **RETIRED:** `documentation.version`. The served `info.version` stays
the spec package version (`SPEC_VERSION`, written by
`build-openapi.ts`), as the #11646 ruling (`5405070350`) set it. An
authored `version` is now refused by a `retiredKey()` tombstone. Its
message names the protocol version as the one source and points an app's
own release number to `description`.
The ruling's execution parameters, each applied:
- `title` is now `.optional()`. The materialized `'ObjectStack API'`
default was never served; the artifact's `'ObjectStack REST API'` stays
the default.
- `contact` and `license` replace the bundled object whole, never member
by member.
- The overlay lives in `registerOpenApiEndpoints` and applies on both
doors.
- `api.version` and the runtime version never enter `info.version`.
- `documentation.version` has an ADR-0087 D3 entry (family
`rest-api-documentation`).
- The ledger rows flip, with the pin as evidence.
- The four pins (1)–(4) are below.
## Premises measured before editing (origin/main `fc0db22b`)
I used a scratch single-file vitest (deleted, never committed), run
under the verify lock after building the rest dependency closure. It
booted `RestServer` plus `registerRoutes()` with project scoping on,
then drove both `openapi.json` handlers:
| authored `documentation` | normalized config carries it | served
`info` (both doors) | same object as the cached artifact `info` |
|:--|:--|:--|:--|
| absent | — | equals the artifact | yes |
| `{}` | `{ title: 'ObjectStack API' }` (the materialized default) |
equals the artifact | yes |
| all nine, `version` included | all nine | equals the artifact: **0 of
9 honoured** | yes |
| `{ description }` only | plus the materialized `title` | equals the
artifact | yes |
| `{ license: { name: 'MIT' } }` | plus the materialized `title` |
equals the artifact | yes |
The scoped door answered the same `info` as the unscoped one in every
case. The handler's clone is shallow, so `enriched.info` is the cached
artifact's own object. The overlay therefore builds a NEW object and
never writes into it (reverse-verified, leg B below).
## Accept / serve changes, each one pinned
| Door | Input | Before | After | Pin |
|:--|:--|:--|:--|:--|
| `GET /api/v1/openapi.json` and `GET
/api/v1/environments/:environmentId/openapi.json` | all eight identity
members | bundled `info` | each member served; `info.version` is still
the spec package version | `rest-openapi-info-overlay.test.ts` (1) |
| same two doors | nothing authored / `documentation: {}` | bundled
`info` | bundled `info`, byte-identical | same file (2); the #11646
whole-block pin in `rest-openapi-route.test.ts` is unedited and kept as
the no-config control |
| same two doors | one key alone (each of `title`, `description`,
`termsOfService`, `contact`, `license`) | bundled `info` | equals the
artifact except that key | same file (2) |
| same two doors | `license: { name: 'MIT' }` | bundled Apache-2.0
licence | `{ name: 'MIT' }`, no `url` | same file (4) |
| same two doors | `contact: { email }` | bundled contact | `{ email }`
only | same file |
| same two doors | `version: 'v9'` plus an overlay, with
`OS_RUNTIME_VERSION` set to a sentinel | — | `info.version` is still the
artifact's; no sentinel anywhere in `info` | same file (1) |
| `RestApiConfigSchema` / `RestServerConfigSchema` |
`documentation.version` (`'2.3.0'`, `'17.4.0'`, `''`) | accepted,
ignored | refused: `invalid_type` at `['documentation', 'version']` with
the prescription |
`packages/spec/src/api/rest-api-config-dead-keys-retirement.test.ts` (3)
|
| `new RestServer(…)` and `createRestApiPlugin(…).start` |
`documentation.version` | constructed, ignored | throws; the message
locates `api.documentation.version`, names `RestApiConfigSchema` and
carries the prescription |
`packages/rest/src/rest-api-config-dead-keys-refused.test.ts` (3) |
| TypeScript `RestApiConfig` (input) | `documentation.version` |
`string` | `never`: a tsc error at the authoring site | an
`@ts-expect-error` pin, held by `check:test-typecheck` |
| `RestApiConfigSchema.parse({ documentation: {} })` | — | `{ title:
'ObjectStack API' }` | `{}` | spec retirement test CONTROL; rest
`rest-config-parse-not-cast.test.ts` §D (both flipped with the new
substance asserted) |
Refusal text, verbatim. It is also the regenerated reference row, which
`gen:docs` prefixes with `[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.
"Protocol version" is spelled out as the package version on purpose. The
in-repo constant `PROTOCOL_VERSION` (`17.0.0`) is a different value from
the served `info.version` (the package's `17.4.0`), and the ruling means
the latter.
## The kit
- **Schema**, `packages/spec/src/api/rest-server.zod.ts`, the
`documentation` block only:
- `title` is `.optional()`.
- The eight members carry describes that name the served `info` field.
- `version` is a `retiredKey()` tombstone with an in-schema comment,
next to the `enabled` tombstone. The inline object is a non-strict
`z.object()`, so a bare deletion would strip the key in silence.
- **REST server**, `packages/rest/src/rest-server.ts`, only the
`registerOpenApiEndpoints` region (three hunks between the discovery
registrar and `loadOpenApiSpec`):
- A pure `private static overlayDocumentationInfo(bundled,
documentation)` sits directly above the method. It returns `bundled`
itself when nothing is authored and a new object otherwise, with a
closed member list: no spread of the block.
- One handler line, `enriched.info =
RestServer.overlayDocumentationInfo(…)`, runs before `res.json`. The
same closure serves both doors.
- The #11646 comment there is rewritten as "unset = the artifact's;
authored = the publisher's; `version` never".
- **ADR-0087:**
- `RETIRED_KEYS_BY_MAJOR[18]` gains
`api/RestApiConfig:documentation.version`
(`entries/retired-keys/18.api__RestApiConfig__documentation.version.ts`).
- The family D3 entry is `rest-api-documentation-version-retired`
(`entries/semantic/18.rest-api-documentation-version-retired.ts`).
- `registry.ts` is regenerated by `gen:migration-registry`.
- There is no D2 conversion: a `RestServerConfig` is plugin TS
configuration, never a stack collection member or a stored row. This
matches the `rest-api-config-dead-keys-retired` precedent on this same
block.
- The route therefore owes exactly two registrations, the exact-key D2
table row and one D3 semantic entry, and no conversion id in
`MIGRATIONS_BY_MAJOR[18].conversionIds`.
- **Ledger**, `packages/spec/liveness/rest_api.json`:
- `title`, `description`, `termsOfService`, the `contact` / `license`
containers and their five members flip to `live`. Evidence is
`packages/rest/src/rest-server.ts#overlayDocumentationInfo` plus the pin
file; the producer is `normalizeConfig`.
- The `version` row stays `dead` with a REMOVED note.
- A `_note` addendum records the change.
- The generated `state-counts.md` moves `rest_api` from 12 live / 12
dead to 20 / 4. The README's hand-written `rest_api` row is edited to
match.
- **Generated:** `content/docs/references/api/rest-server.mdx`.
- **Tests:**
- The new `packages/rest/src/rest-openapi-info-overlay.test.ts`.
- A `[#20294]` block in `rest-api-config-dead-keys-refused.test.ts`.
- A `documentation.version` block in the spec retirement test. Its
tree-scoped absence walk now also matches a `documentation` object
carrying `version`, with anti-vacuity cases in `.ts`, `.json` and
`.yaml`, plus two neighbour cases that must not match: the route
identifier `api.version`, and the enforced members alone.
- Two fixtures in `packages/spec/src/api/rest-server.test.ts` that
authored `documentation.version: '1.0.0'` drop it.
- One comment in `rest-api-config-defaults-follow-spec.pin.test.ts`.
- **Changeset**, `.changeset/20294-openapi-info-publisher-overlay.md`:
- `@objectstack/spec` `minor` and `@objectstack/rest` `minor` (both in
the `fixed` group).
- BREAKING banner, `Clause-②: yes (narrowing)`, FROM → TO with the
one-line fix.
- The ADR-0087 disposition marker `registered
rest-api-documentation-version-retired`, in the changeset body where
`check:adr-0087-registration` reads it.
**On the `Clause-②` spelling.** The value is the ruling's `yes`: a
behaviour change on a public document, which now publishes owner-set
`termsOfService` / `contact.email` it never carried. The `(narrowing)`
arm is required because the retirement refuses a formerly accepted
input. `check-adr-0087-registration.mjs#breakingDeclaration` reads that
arm, as well as the `**BREAKING**` banner, as a breaking declaration.
The `adr-0087:` marker lives in the changeset, not in this body. This
body carries only the declaration line that `Check Changeset` reads.
## Verification, at HEAD `69b4d203`
`69b4d203` is `origin/main` `2b24b8b8` merged in through
`scripts/pm/os-regen-merge.sh`. No generated path needed regeneration
after the merge: `check:generated` read all 15 artifacts current. After
the merge I ran `pnpm install --frozen-lockfile` and rebuilt the rest
dependency closure (`pnpm --filter '@objectstack/rest^...' build`, which
includes spec). Every heavy run went through
`scripts/pm/os-verify-lock.sh`, and every exit code was written to disk
before any pipe.
- `pnpm --filter @objectstack/rest exec vitest run --project local`:
exit 0 · 219 files · 4185 passed, 34 skipped.
- `pnpm --filter @objectstack/rest exec vitest run --project repo`: exit
0 · 1 file · 8 passed.
- `pnpm --filter @objectstack/spec exec vitest run --project local`:
exit 0 · 572 files · 16794 passed, 1 todo.
- `pnpm --filter @objectstack/spec exec vitest run --project repo`: exit
0 · 38 files · 697 passed. The tree-scoped absence walk runs here.
- `pnpm --filter @objectstack/rest run typecheck` and `pnpm --filter
@objectstack/spec run typecheck`: both exit 0. Each is `tsc --noEmit`
plus `check:test-typecheck`; spec also runs `check:scripts-typecheck`. A
green `check:test-typecheck` is what proves the new `@ts-expect-error`
pin bites, since an unused one would add a TS2578 signature.
- `pnpm --filter @objectstack/spec check:generated`: exit 0, all 15
artifacts current. `pnpm --filter @objectstack/spec check:liveness`:
exit 0.
- **Gates.** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 115 commands at this head. The
`--ran` reconciliation reads: **115 derived — 113 run, 0 NOT-MEASURED, 2
UNRUN.** Of the 113, 112 exit 0 and 1 exits 1:
- `pnpm check:platform-checklist` exits 1 with
`areas/identity-auth.json: ABSENT SYMBOL —
packages/plugins/plugin-auth/src/auth-plugin.ts#twoFactor`. This red is
inherited from `main`: this diff touches neither `docs/qa/**` nor
`packages/plugins/plugin-auth/**` (0 diff lines against `origin/main`
there). The anchor went stale when `7d630889` turned the literal
`twoFactor: true` into an inline `plugins: { twoFactor: true }` object.
The gate is deliberately outside per-PR CI (the `lint.yml` comment near
the checklist step).
- `check:skill-examples` first exited 3 (PREREQUISITE NOT MET: no
`client-react` declarations). It went green after building
`@objectstack/client` and `@objectstack/client-react`: 259 prose
examples type-check.
- UNRUN, declared, not measured locally: `pnpm
check:dual-build-cjs-loads`, which reads every package's `dist/` (a
whole-tree build is outside this card's scope; CI's Build Core builds
it), and `pnpm check:type-check-debt`, whose `--re-measure` rebuilds
every package. This diff touches no DEBT or EXEMPT package, and
`check:type-check-coverage` ran with exit 0.
- **Lint, narrowed and shown not to exclude anything.** I ran `eslint
--no-inline-config --format json` over the 11 changed code files: 11
files, 0 errors, 0 warnings. The population comes from
`eslint.config.mjs`'s catch-all `files:
['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']`, which all 11 are in. The
config never enables type-aware linting (no `parserOptions.project`, no
typed rules, as stated in the config itself), so this diff cannot move
the verdict on any untouched file. The repo-wide `pnpm lint` is CI's.
`turbo ls --affected` was not used to narrow anything; consumer packages
were not rebuilt. The public surface that moves is the `RestApiConfig`
input type (`documentation.version` becomes `never`) and the parsed
`title` (`string | undefined`). The retirement playbook's tree-scoped
absence pin is the sweep for authoring sites. It walks `packages`,
`examples`, `skills`, `content` and `scripts` and finds none. No
non-test reader of `documentation.title` exists outside `packages/rest`.
### Reverse verification (one-time; committed head `69b4d203`)
Every mutation went through `scripts/ablation-replace.mjs`: the anchor
hit exactly once, the blob change was verified on disk, and the restore
was proven by blob == HEAD with an empty `git diff HEAD`. Tree status
was empty before and after. Each subject is imported from source
(`./rest-server`, `./rest-server.zod`), so no `dist/` preflight applies.
Every direction observed is the expected one, red.
- **Control:** `rest-openapi-info-overlay.test.ts` on the unmutated
tree: 11/11 passed.
- **A: the overlay line deleted.** The mutation was `enriched.info =
RestServer.overlayDocumentationInfo(…);` → a comment. Blob `3f01f03a` →
`5c7dde2c`. Result: **9 failed | 2 passed**. The two survivors are
exactly the nothing-authored controls. Restored (blob == HEAD).
- **B: the helper writes into the cached artifact.** The mutation was
`return { ...bundled, ...authored };` → `return Object.assign(bundled ??
{}, authored);`. Blob `3f01f03a` → `662c8f32`. Result: **3 failed | 8
passed**: the cache-unchanged assertion in (1), and the positive
controls of (4) and of partial-`contact`, whose artifact licence URL /
contact name had been overwritten. Restored.
- **C: the tombstone neutralised.** The mutation was `version:
retiredKey(` → `version: z.string().optional() ?? retiredKey(`. Blob
`e69191ec` → `1acff1f8`. Result on the spec retirement test: **5 failed
| 18 passed**, which are the three parse doors, the whole-config door
and the tsc pin's parse leg. Restored.
- **D: cross-package type proof.** I planted `const probe20294:
RestApiConfig = { documentation: { title: 'Acme', version: '2.3.0' } }`
in `packages/rest/src` and ran `pnpm --filter @objectstack/rest exec tsc
--noEmit`. It exited 1 with `src/zz-typeproof-20294.ts(3,76): error
TS2322: Type 'string' is not assignable to type 'undefined'` and 0
errors elsewhere. That proves rest's tsc reads the rebuilt spec `.d.ts`.
The probe was then removed and its absence confirmed.
## Acceptance notes
- **Stale comments outside the fenced region, not edited.** The dispatch
fences `rest-server.ts` to the `registerOpenApiEndpoints` region, so two
comments are left untouched:
- The `NormalizedRestServerConfig.api.documentation` comment (near
`:1116`) still says a written `documentation` "arrives with its OWN
declared inner defaults applied (`.title`)". There are none now.
- The `parseDeclaredApiConfig` docblock's #14366 paragraph (near
`:4112`) says `documentation` has "zero read sites outside this block".
Since this change it has one, the overlay.
Neither comment is emitted into a published `.d.ts`: the type is
module-local, and the method is private. The one-line fix is to append
"(none since #20294 made `title` optional)" to the first and "(until
#20294's overlay)" to the second. Carrier: whoever next touches either
region.
- **Two title literals not covered by this ruling:**
- The `/docs` viewer page's HTML `title` stays the literal `ObjectStack
API Docs`. It is the browser-tab title of the Scalar page, not an `info`
field. The viewer renders the served document, overlaid `info.title`
included, in the page body. No declared contract ties the tab to
`info.title`, and no key authors it, so this is polish (class a/b/c:
none). Carrier: none.
- `OpenApiGenerationConfigSchema.title`
(`packages/spec/src/api/plugin-rest-api.zod.ts`) carries a third
`'ObjectStack API'` default. It is dormant: 0 non-test readers under
`packages/**` (only migration-entry prose names the schema), against a
lit control (`RestApiConfigSchema` found in `rest-server.ts`). It has no
runtime parser (`packages/rest` declares its own `RestApiPluginConfig`
interface) and no ledger enrolment. Not filed; carrier: none.
- **Release-owned files, not edited:** `packages/rest/CHANGELOG.md` and
`packages/spec/CHANGELOG.md`. The changeset is their input. The pending
`.changeset/14640-rest-api-liveness-ledger.md` still describes
`documentation` as changing nothing, which was true when it landed. The
release compiles it next to this changeset.
- **Reachability is unchanged:** `RestServerConfig` is embedder-only.
`os serve` forwards only `enableProjectScoping` / `projectResolution`,
so every CLI-started deployment serves the same document as before.
- **Governed surfaces:** none touched (no `.claude/**`, `docs/adr/**`,
`skills/**`, `AGENTS.md`).
**Closing target #20359:** the seat claimed it for this branch, closure
claim `5878320739` on #20359. That is the ruling carrier; its ruling
`5862477514` orders this PR to close it.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 9e1689f commit 80153f5
16 files changed
Lines changed: 777 additions & 102 deletions
File tree
- .changeset
- content/docs/references/api
- packages
- rest/src
- spec
- liveness
- src
- api
- migrations
- entries
- retired-keys
- semantic
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
250 | 250 | | |
251 | 251 | | |
252 | 252 | | |
253 | | - | |
| 253 | + | |
254 | 254 | | |
255 | 255 | | |
256 | 256 | | |
257 | 257 | | |
258 | 258 | | |
259 | 259 | | |
260 | 260 | | |
261 | | - | |
262 | | - | |
263 | | - | |
264 | | - | |
265 | | - | |
266 | | - | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
267 | 267 | | |
268 | 268 | | |
269 | 269 | | |
| |||
298 | 298 | | |
299 | 299 | | |
300 | 300 | | |
301 | | - | |
| 301 | + | |
302 | 302 | | |
303 | 303 | | |
304 | 304 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
17 | 22 | | |
18 | 23 | | |
19 | 24 | | |
| |||
94 | 99 | | |
95 | 100 | | |
96 | 101 | | |
| 102 | + | |
| 103 | + | |
97 | 104 | | |
98 | 105 | | |
99 | 106 | | |
| |||
139 | 146 | | |
140 | 147 | | |
141 | 148 | | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
142 | 182 | | |
143 | 183 | | |
144 | 184 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
35 | 35 | | |
36 | 36 | | |
37 | 37 | | |
38 | | - | |
39 | | - | |
40 | | - | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
41 | 43 | | |
42 | 44 | | |
43 | 45 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
334 | 334 | | |
335 | 335 | | |
336 | 336 | | |
337 | | - | |
| 337 | + | |
338 | 338 | | |
339 | 339 | | |
340 | 340 | | |
341 | | - | |
342 | | - | |
343 | | - | |
344 | | - | |
345 | | - | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
346 | 348 | | |
347 | 349 | | |
348 | 350 | | |
349 | 351 | | |
350 | 352 | | |
351 | 353 | | |
352 | | - | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
353 | 364 | | |
354 | 365 | | |
355 | 366 | | |
| |||
0 commit comments