Repository navigation
Commit 26daf0b
feat(spec,rest)!: retire api.responseFormat and api.documentation.enabled (ADR-0049) (#20343)
Fixes #20295
Clause-②: no (narrowing)
## Summary
Retires two keys of `RestServerConfig.api` (`RestApiConfigSchema`) under
ADR-0049 enforce-or-remove — triage's grade, verbatim: 「Verdict:
**RETIRE** the 4 keys, by the maintainer's criterion」.
- `api.responseFormat` — the whole block (`envelope`, `includeMetadata`,
`includePagination`): one `retiredKey()` tombstone for the container.
- `api.documentation.enabled` — a tombstone inside the still-live
`documentation` block. Its other members (`title`, `description`,
`version`, `termsOfService`, `contact`, `license`) are untouched: they
belong to the sibling card #20294.
Both keys were parsed, defaulted and copied into `RestServer`'s config
by `normalizeConfig`, and nothing read them back. `envelope: false`
unwrapped no response. `documentation.enabled: false` turned no document
off, because `api.enableOpenApi` decides that mount. Now each key
carries its prescription at the schema, `RestServer` construction
refuses it (so does the REST plugin's `start`), and `normalizeConfig`
neither forwards nor re-defaults it.
## Accept / refuse changes — every one pinned
| Door | Input | Before | After | Pin |
|:--|:--|:--|:--|:--|
| `RestApiConfigSchema` | `responseFormat: {…}` — `{ envelope: false }`,
the old defaults, `{ includePagination: false }`, `{}` | accepted, inner
defaults filled | refused: `invalid_type` at `['responseFormat']`,
message = prescription below |
`packages/spec/src/api/rest-api-config-dead-keys-retirement.test.ts` |
| `RestServerConfigSchema` | `api.responseFormat` | accepted | refused
at `api.responseFormat`, same prescription | same file |
| `RestApiConfigSchema` | `documentation.enabled: false` / `true` (the
old default) | accepted (default `true`) | refused: `invalid_type` at
`['documentation', 'enabled']`, prescription below | same file |
| `RestServerConfigSchema` | `api.documentation.enabled` | accepted |
refused at `api.documentation.enabled` | same file |
| `new RestServer(…)` and `createRestApiPlugin(…).start` | either key |
constructed; the key was ignored | throws; the message names
`api.responseFormat` / `api.documentation.enabled`,
`RestApiConfigSchema` and the prescription |
`packages/rest/src/rest-api-config-dead-keys-refused.test.ts` |
| `RestApiConfigSchema` | `documentation: {}` | parsed to `{ enabled:
true, title }` | parsed to `{ title }` | spec retirement test (control),
`rest-server.test.ts`, `rest-config-parse-not-cast.test.ts` §D |
| TypeScript `RestApiConfig` (input) | either key | object / boolean |
`never` (a tsc error at the authoring site) | two `@ts-expect-error`
pins, held by `check:test-typecheck` |
Unchanged, pinned as controls: every live key of the `api` block parses
as before, including `documentation`'s other members; a config without
the two keys mounts the same route set (`mounted({ documentation: {
title, description } })` equals `mounted({})`); `enableOpenApi: false`
removes exactly `GET /api/v1/docs` and `GET /api/v1/openapi.json`.
### Refusal texts (verbatim; both are also the new `.describe()` text,
prefixed `[REMOVED] `)
> `api.responseFormat` was removed in @objectstack/spec 17.5.0 (ADR-0049
enforce-or-remove) — nothing ever read it: `envelope`, `includeMetadata`
and `includePagination` were parsed, defaulted and copied into the REST
server's config and never consulted, so `envelope: false` unwrapped no
response. Delete the key. Response shapes are fixed, not a server-wide
option: each route answers in the response schema
`@objectstack/spec/api` declares for it, which is what the client SDK
parses and the served /openapi.json describes, so no configuration
changes them.
> `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.
Construction refusal (unchanged envelope, new lines): ``REST API
configuration is invalid: `api` does not satisfy `RestApiConfigSchema`
(@objectstack/spec/api), the schema that declares it.`` followed by ` -
api.responseFormat: PRESCRIPTION` (or `api.documentation.enabled`).
Describes removed with the keys: "Response format options", "Wrap
responses in standard envelope", "Include response metadata (timestamp,
requestId)", "Include pagination info in list responses", "Enable API
documentation".
No `os migrate meta` sentence in either prescription, on purpose: there
is no D2 conversion for the command to list (see below), matching the
sibling `crud.*` / `metadata.*` / `batch.*` tombstones on this same
file.
## Premises measured before editing (origin/main 4e0f72e)
1. **Nothing reads the four keys.** `packages/**` non-test code: 0 reads
— the only code sites were `NormalizedRestServerConfig`'s type and
`normalizeConfig`'s own write. Lit control on the same instrument:
`enableOpenApi` finds its read at `registerRoutes`. A spread /
whole-block-destructure sweep over `packages/rest/src` (non-test)
returns only the parse call and `const { enableProjectScoping,
projectResolution } = this.config.api`. objectui at its pin `f8a9d0fb`:
0 for `RestApiConfig|RestServerConfig`, `responseFormat`,
`includePagination`, `enableOpenApi` (control `basePath` = 184). cloud
at `96eb092`: 0 authoring sites for the same terms and
`documentation.enabled` (control `createRestApiPlugin` = 11; every call
forwards the stack's own top-level `api:` block, whose schema carries
neither key).
2. **Producers.** None outside the kit: the in-repo authors were four
spec test cases, two rest test cases and the `@example` in the schema
docblock; all are converted (the example now shows `enableOpenApi`). No
example app, skill, form, i18n bundle or hand-written doc authors either
key.
3. **Route.** `RestApiConfigSchema` and its inline `documentation`
object are non-strict `z.object()`s ⇒ `retiredKey()` tombstones (a bare
deletion would strip the key in silence, ADR-0104). Ledger rows stay
`dead` with a REMOVED note; `responseFormat`'s three child rows collapse
into its one row, because the tombstone is a leaf and child rows would
report ORPHAN (the `crud.patterns` precedent).
4. **`normalizeConfig`** no longer lists `responseFormat`;
`documentation` still passes through, so the retired `enabled` is
neither forwarded nor re-defaulted (pinned).
## Choices settled here (four axes)
- **`responseFormat` retired as one container tombstone, not three
member tombstones.** Business need: no author and no reader of any
member. Long-term: a container whose every member is retired would keep
accepting `responseFormat: {}` — an empty knob a reader takes for a
capability. AI-safety: one refusal on the key an author actually types;
`{}` is refused too. Scope: one tombstone, one ledger row (the
`crud.patterns` precedent, which also collapsed child rows).
- **The server REFUSES (the `crud.patterns` posture), it does not
`.omit()` and ignore (the `requireAuth` posture).** No boot path or
shipped config writes either key, so nothing chose warn-and-ignore for
them; a silent `.omit()` would recreate the strip this retirement
removes. Ablation C below shows the difference is measurable.
- **A tree-scoped absence pin was added** (the retirement playbook's
default sweep), over the radius `@objectstack/spec` already declares in
`scripts/cross-package-test-inputs.mjs`; no new declaration was needed
(`check:cross-package-test-inputs` green). Its matcher is structural so
that the agent alias map's `responseFormat: 'structuredOutput'` and an
OpenAI-style `responseFormat: { type }` never match.
## The retirement kit
- **Schema** `packages/spec/src/api/rest-server.zod.ts` — two tombstones
with in-schema comments; docblock example converted.
- **REST server** `packages/rest/src/rest-server.ts` — only the
`NormalizedRestServerConfig.api` type (near the old `:1105`) and the
`parseDeclaredApiConfig` / `normalizeConfig` region. None of the regions
#20319 edited were touched; this branch was rebuilt on main after #20319
landed.
- **ADR-0087** — `RETIRED_KEYS_BY_MAJOR[18]` gains
`api/RestApiConfig:responseFormat` and
`api/RestApiConfig:documentation.enabled` (one entry file each); one D3
entry for the family, `rest-api-config-dead-keys-retired` (ruling B on
#17152); no D2 conversion, because a `RestServerConfig` is plugin TS
configuration, never a stack collection member or a stored row. The
generated regions of `registry.ts` are regenerated.
- **Ledger** `packages/spec/liveness/rest_api.json` (both rows REMOVED,
`cross-repo` scope, `_note` addendum), `liveness/README.md` row,
generated `state-counts.md` (`rest_api` 14 → 12 dead, 26 → 24
classified).
- **Generated** `authorable-surface/api.json` (`responseFormat` →
`[RETIRED]`), `content/docs/references/api/rest-server.mdx`,
`docs/audits/2026-07-unknown-key-strictness-ledger.counts.md` (`api/`
432 → 431 sites: the inline `responseFormat` object left).
- **Tests** — the two new pin files above (the spec one runs in the
`repo` project); converted cases in `rest-server.test.ts`,
`rest-config-parse-not-cast.test.ts` §D and one comment in
`rest-api-config-defaults-follow-spec.pin.test.ts`.
- **Changeset** `.changeset/20295-rest-api-config-dead-keys-retired.md`
— `@objectstack/spec` and `@objectstack/rest` `minor`, BREAKING banner,
FROM → TO with the one-line fix, and the ADR-0087 disposition
`registered rest-api-config-dead-keys-retired`.
## Verification — at HEAD `6f07f0c1`
All heavy runs went through `scripts/pm/os-verify-lock.sh`; every exit
code below was captured to disk before any pipe. `6f07f0c1` is
`origin/main` `a78f731a` merged in (after #20319 landed) through
`scripts/pm/os-regen-merge.sh`, then the rest dependency closure
rebuilt.
- `pnpm --filter @objectstack/rest exec vitest run --project local` —
exit 0, 204 files, 3680 passed, 1 skipped.
- `pnpm --filter @objectstack/spec exec vitest run --project local` —
exit 0, 554 files, 16353 passed, 1 todo.
- `pnpm --filter @objectstack/spec exec vitest run --project repo` —
exit 0, 34 files, 620 passed (the new tree-scoped pin runs here).
- `pnpm --filter @objectstack/spec run typecheck` and `pnpm --filter
@objectstack/rest run typecheck` — both exit 0 (`tsc --noEmit` plus
`check:test-typecheck`; the spec one also `check:scripts-typecheck`).
`check:test-typecheck` green is what proves the two `@ts-expect-error`
pins bite: an unused one would be a new TS2578 signature.
- `pnpm --filter @objectstack/spec run check:generated` — exit 0, all 15
generated artifacts current at this head.
- Gates: `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 115 commands for this diff at this
head; `--ran` reconciliation: **115 accounted — 113 run, all exit 0; 2
NOT MEASURED; 0 unrun.**
- NOT MEASURED `pnpm check:dual-build-cjs-loads` — exit 3, PREREQUISITE
NOT MET: it reads every package's `dist/`, and a whole-tree build is
outside this card's local scope (CI's Build Core builds it).
- NOT MEASURED `pnpm check:type-check-debt` — its `--re-measure`
rebuilds every package and re-runs tsc over every DEBT / EXEMPT entry;
this diff touches no DEBT or EXEMPT package (CI's Lint job). `pnpm
check:type-check-coverage` ran, exit 0.
- Consumer fixture triage: the tree-scoped absence pin is the sweep — it
walks `packages`, `examples`, `skills`, `content` and `scripts` and
finds no `api` block authoring either key anywhere (anti-vacuity: more
than 1000 files visited, more than 5 of them spell `responseFormat`).
The near consumers that parse or type this config were also run:
`@objectstack/core` `src/qa/http-adapter.test.ts` (it parses
`RestApiConfigSchema.parse({})` for the route prefix) — exit 0, 32
passed; `@objectstack/client` `client.data-prefix.test.ts` +
`client.metadata-prefix.test.ts` (they type a `RestServerConfig`) — exit
0, 13 passed. The rest of the downstream closure (`pnpm --filter
'...^@objectstack/rest'`: cli, runtime, hono, the examples, …) was NOT
rebuilt or run locally, by the retirement playbook's rule that the
absence pin, not a consumer-closure rebuild, is the sweep; CI runs them.
### Reverse verification (one-time; each mutation through
`scripts/ablation-replace.mjs`, restored and proven by blob == HEAD and
an empty `git diff HEAD`)
Run at `2e575f15` (this branch before the second merge of main; the
merge touched none of the three mutated files). Every direction observed
is the expected one: red.
- **A — the schema tombstone.** `responseFormat: retiredKey(` →
`responseFormat: z.any().optional() ?? retiredKey(` in
`rest-server.zod.ts` (anchor 1 → 0, blob `32ebfe55` → `bfbb9755`). Spec
retirement test: **6 failed | 10 passed** — the four `responseFormat`
door pins, the whole-config door, and the tsc pin's parse leg. Restored
(blob == HEAD); control rerun 16/16.
- **B — the tree-scoped pin.** First attempt was a NO-OP and is not
counted: its replacement contained the anchor, so `ablation-replace`
refused (anchor 1 → 1), restored, and never ran the test. Redone with a
disjoint spelling: `responseFormat: { envelope: false } as never,`
planted before `enableDiscovery: true,` in
`packages/spec/src/api/rest-server.test.ts` (anchor 1 → 0, blob
`794c1326` → `0766f4e0`). **1 failed | 15 passed** — the absence pin,
naming `packages/spec/src/api/rest-server.test.ts:644`. Restored (blob
== HEAD).
- **C — the server's parse.** `RestApiConfigSchema.omit({ requireAuth:
true })` → `.omit({ requireAuth: true, responseFormat: true })` in
`rest-server.ts` — the silent-strip posture (anchor 1 → 0, blob
`abf25fa0` → `e93b894a`). Rest refusal test: **3 failed | 5 passed** —
the `responseFormat` refusal, its no-`api.version` positive control and
the plugin path; the `documentation.enabled` pins stay green, as they
must. Restored (blob == HEAD); control rerun 8/8.
After all four legs: `git diff HEAD` empty, each blob equal to its HEAD
blob. The rest test's subject is `./rest-server.ts` (source, no alias
hop), and the spec test's subject is `./rest-server.zod.ts` (source), so
no `dist/` preflight applies.
## Acceptance notes
- **`packages/rest/CHANGELOG.md`** is in the card's file surface but is
release-owned (AGENTS.md Documentation Guardrails): not edited; the
changeset is its input.
- **`.changeset/14640-rest-api-liveness-ledger.md`** (pending, another
card's) says `documentation` and `responseFormat` "are accepted,
validated and normalized, and change nothing" — true when it landed; not
edited here. The release compiles it next to this changeset.
- **Clause-② value.** The claim carried `Clause-②: yes`, from triage's
execution note. Measured on this diff: no accept set widens and no
export is added; the only additions are ADR-0087 ledger registrations,
which the two nearest retirements (#20227, #20238) declared under `no`.
On that measurement the seat answered `no (narrowing)`, and this body
and the changeset carry it. The gate readings are unchanged (`minor`,
declared breaking, `registered`).
- **Observed, not filed (dormant):**
`RestApiPluginConfigSchema.responseEnvelope` in
`packages/spec/src/api/plugin-rest-api.zod.ts` is a second declared
envelope toggle. The spec schema has no runtime parser (`packages/rest`
declares its own `RestApiPluginConfig` interface) and is not enrolled in
any liveness ledger. No reach was measured, so nothing is filed.
Carrier: none.
- No governed surface is touched (`docs/audits/**` is not on the
register).
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent f39ea95 commit 26daf0b
19 files changed
Lines changed: 1002 additions & 108 deletions
File tree
- .changeset
- content/docs/references/api
- docs/audits
- packages
- rest/src
- spec
- authorable-surface
- 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
249 | 249 | | |
250 | 250 | | |
251 | 251 | | |
252 | | - | |
253 | | - | |
| 252 | + | |
| 253 | + | |
254 | 254 | | |
255 | 255 | | |
256 | 256 | | |
257 | 257 | | |
258 | 258 | | |
259 | | - | |
| 259 | + | |
260 | 260 | | |
261 | 261 | | |
262 | 262 | | |
263 | 263 | | |
264 | 264 | | |
265 | 265 | | |
266 | 266 | | |
267 | | - | |
268 | | - | |
269 | | - | |
270 | | - | |
271 | | - | |
272 | | - | |
273 | | - | |
274 | | - | |
275 | 267 | | |
276 | 268 | | |
277 | 269 | | |
| |||
305 | 297 | | |
306 | 298 | | |
307 | 299 | | |
308 | | - | |
309 | | - | |
| 300 | + | |
| 301 | + | |
310 | 302 | | |
311 | 303 | | |
312 | 304 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
257 | 257 | | |
258 | 258 | | |
259 | 259 | | |
260 | | - | |
| 260 | + | |
261 | 261 | | |
262 | 262 | | |
263 | 263 | | |
| |||
Lines changed: 170 additions & 0 deletions
| 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 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
Lines changed: 2 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
39 | | - | |
| 39 | + | |
| 40 | + | |
40 | 41 | | |
41 | 42 | | |
42 | 43 | | |
| |||
0 commit comments