Skip to content

feat(spec,rest)!: retire api.responseFormat and api.documentation.enabled (ADR-0049) - #20343

Queued
objectstack-fleet[bot] wants to merge 10 commits into
mainfrom
claude/issue-20295-rest-api-retire
Queued

objectstack-fleet[bot] wants to merge 10 commits into
mainfrom
claude/issue-20295-rest-api-retire

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

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」.

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 fix(runtime,rest): the dispatcher /meta list prunes what RestServer's list prunes, through one list gate (#20237) #20319 edited were touched; this branch was rebuilt on main after fix(runtime,rest): the dispatcher /meta list prunes what RestServer's list prunes, through one list gate (#20237) #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 [Decision] 一次退役,要写一条记录还是两条?—— 迁移条目的 D2/D3 约定,两处成文相互矛盾 #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 (feat(spec)!: retire the view item's owner and hidden keys (ADR-0049) #20227, fix(spec): a joined report draws no chart — retire blocks[].chart and refuse a joined container chart (#20161) #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

…(ADR-0049) — wip

Tombstones, ledger rows, D3 + RETIRED_KEYS entries, RestServer normalizeConfig
stops forwarding the retired block.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN
…r and across the tree; regenerate docs and counts

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN
…number that no longer resolves

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Sep 28, 2026
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/rest, @objectstack/spec, touching 7 documentable anchor(s). ⚠️ 7 changed file(s) yielded no anchor (packages/spec/authorable-surface/api.json, packages/spec/liveness/README.md, packages/spec/liveness/rest_api.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

⛔ 4 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx (via RestServer (symbol, a top-level class))
  • content/docs/releases/v12.mdx (via RestApiConfigSchema (symbol, a top-level const), RestServer (symbol, a top-level class))
  • content/docs/releases/v16.mdx (via RestServer (symbol, a top-level class))
  • content/docs/releases/v17/17-3.mdx (via RestServer (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 7 changed file(s) yielded no anchor (packages/spec/authorable-surface/api.json, packages/spec/liveness/README.md, packages/spec/liveness/rest_api.json, …) — pages documenting those are invisible to this run
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 138 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json c74de10a94616a91bf496898077894fd913da1d7 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 23fa137f5272e32ed16e4d531756bf3c16658f24 — the merge of head f57d2e433c375860ac518733e2d642ca6e5b3f07 into base c74de10a94616a91bf496898077894fd913da1d7, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 23fa137f5272e32ed16e4d531756bf3c16658f24 && git checkout 23fa137f5272e32ed16e4d531756bf3c16658f24
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c74de10a94616a91bf496898077894fd913da1d7 f57d2e433c375860ac518733e2d642ca6e5b3f07 && git checkout -B drift-repro c74de10a94616a91bf496898077894fd913da1d7 && git merge --no-ff f57d2e433c375860ac518733e2d642ca6e5b3f07

node scripts/docs-audit/affected-docs.mjs --json c74de10a94616a91bf496898077894fd913da1d7

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs c74de10a94616a91bf496898077894fd913da1d7 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5af9ff5a24b1c60437236ef31e2e3143c18bc95a

PR #20343 (card #20295), reviewed at the head the PR carried when the review started, which is the head the brief named; it did not move. Merge-base with origin/main (d3958bac) is a78f731a; 6f07f0c1 → 5af9ff5a is exactly one line (Clause-②: yes (narrowing) → Clause-②: no (narrowing) in the changeset), so every measurement the dev report made at 6f07f0c1 carries to this head unchanged. All measurements below were taken by this reviewer in a detached worktree at 5af9ff5a (pnpm install --frozen-lockfile, exit 0), build-free through tsx over src and git reads, plus one lock-held ablation leg; objectui and cloud were read by git grep against refs only.

① Derived judgments

  1. Zero pull, re-counted — RIGHT. packages/** non-test at the head: responseFormat has 9 matching lines, every one either the tombstone itself, the type comment, the pinned surface anchor (authorable-surface.base.json:1855), or a different key of the same spelling (ai/agent.zod.ts:152, the agent alias map whose value is the string 'structuredOutput'); documentation.enabled / includePagination / includeMetadata / .envelope hit only the schema's own prescription text, the rest-server.ts comments, ResponseEnvelopeConfig on plugin-rest-api.zod.ts (a different schema, no runtime parser) and unrelated includeMetadata options on metadata-persistence, event integrations and the database loader. Zero behaviour-changing readers. Lit control on the same instrument: this.config.api. property reads in rest-server.ts return 1–2 sites for ten sibling keys, enableOpenApi finding its gate at rest-server.ts:4423 (if (this.config.api.enableOpenApi) registerOpenApiEndpoints). Authors: objectui at the .objectui-sha pin f8a9d0fb0596f4521076628e2bbfe27e6ce67d52 — 0 for responseFormat / includePagination / includeMetadata / documentation.enabled / RestApiConfig / RestServerConfig / enableOpenApi against basePath = 184; cloud origin/main 96eb092f — 0 for the same seven against createRestApiPlugin = 11 (the four call sites forward the stack's own top-level api block); examples/** — 0 for every key and for createRestApiPlugin, against defineStack = 64. No author anywhere; nothing to convert.

  2. The tombstones refuse at every door with the right prescription — RIGHT. Measured by tsx on the head source: RestApiConfigSchema refuses responseFormat for { envelope: false }, the old defaults, { includePagination: false } and {} — each invalid_type at ['responseFormat'] with the message opening `api.responseFormat` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) … Delete the key. Response shapes are fixed, not a server-wide option …; documentation.enabled refused for false and true at ['documentation','enabled'] with `api.documentation.enabled` was removed … Delete the key; `api.enableOpenApi: false` is the switch that leaves the `/openapi.json` document and its `/docs` viewer unmounted. RestServerConfigSchema locates the same refusals at api.responseFormat and api.documentation.enabled. The server seam runs RestApiConfigSchema.omit({ requireAuth: true }) (rest-server.ts:1205); measured directly, that schema still carries responseFormat in its shape and refuses both keys identically, and parseDeclaredSubConfig (:1254) wraps every issue as REST API configuration is invalid: `api` does not satisfy `RestApiConfigSchema` (@objectstack/spec/api), the schema that declares it. followed by one indented - api.KEYPATH: PRESCRIPTION line per issue, and throws — loud and located. createRestApiPlugin(…).start constructs new RestServer(…, config.api …) (rest-api-plugin.ts:516), so the same throw is the plugin's refusal; both are pinned in packages/rest/src/rest-api-config-dead-keys-refused.test.ts, which CI's Test Core runs at this head (§8). normalizeConfig (:4251–:4302) lists thirteen api keys and no responseFormat; documentation passes through as parsed, and the parse no longer materialises enabled (parse({ documentation: {} }) → { title: 'ObjectStack API' }, no enabled). Controls: the six other documentation members (title, description, version, termsOfService, contact, license) parse at the head deep-equal to the base output minus the retired enabled; enableOpenApi: false is kept; a config without documentation parses byte-equal to base through RestServerConfigSchema; the only non-byte-equal case is a present documentation block, where the base's enabled: true default is gone — the retirement itself, no other default moved (parse({}) carries no responseFormat at base or head, enableOpenApi still defaults true).

  3. Nothing else narrowed — RIGHT. z.toJSONSchema(RestServerConfigSchema) base vs head, input side: 8 differing paths, all under api.documentation.enabled (default/type gone, not added, [REMOVED] description) and api.responseFormat (type/properties gone, not added, [REMOVED] description); output side: the same plus documentation.required ["enabled","title"] → ["title"] and responseFormat's required/additionalProperties gone. authorable-surface/api.json moves exactly one line (api/RestApiConfig:responseFormat → … [RETIRED]; documentation.enabled is a nested key with no line of its own). rest: the served OpenAPI document's info block comes from api.documentation (9 keys) #20294's keys are untouched: the schema diff replaces only the enabled: line inside documentation, and their six ledger rows keep verifiedAt: 2026-09-21. rest-server.ts fences hold: four hunks at :1115 (the NormalizedRestServerConfig.api type), :4167, :4267 and :4285 (the parseDeclaredApiConfig docblock and normalizeConfig); no import line changes; fix(runtime,rest): the dispatcher /meta list prunes what RestServer's list prunes, through one list gate (#20237) #20319 (cc40033ed4) is in the merge-base. git merge-tree --write-tree origin/main HEAD → 6c34191d clean; against HEAD the merged tree differs only in the fix(driver-sql): a MySQL date reads back the day it stores, so a year below 100 no longer comes back a century late #20306 files (driver-sql, one rest test, two changesets) — the kit files are byte-identical.

  4. ADR-0087 kit — RIGHT. retired-keys/18.api__RestApiConfig__responseFormat.ts and …__documentation.enabled.ts export 'api/RestApiConfig:responseFormat' and 'api/RestApiConfig:documentation.enabled'; semantic/18.rest-api-config-dead-keys-retired.ts has id rest-api-config-dead-keys-retired, surface restServer.api.responseFormat / restServer.api.documentation.enabled, a replacement naming api.enableOpenApi, and its prose matches what was measured (parsed, defaulted, copied, never read; enableOpenApi at the mount). Loaded from src/migrations/registry: RETIRED_KEYS_BY_MAJOR[18] has 190 entries, both keys present, no duplicates, sorted; MIGRATIONS_BY_MAJOR[18].semantic (221 ids) carries the entry once, sorted, and its id is unique across majors; conversionIds has nothing naming either key (no D2, as declared). pnpm --filter @objectstack/spec check:migration-registry → registry.ts is current (298 semantic, 219 retired-key, 199 retired-def), exit 0, so the diff is the generator's. In the merged tree the registry is identical to HEAD's and carries both ids at :13390, :17130, :17150. check:spec-changes and check:upgrade-guide both current (a step-18 neighbour id is not projected either, so no projection change was owed).

  5. Liveness and ledgers — RIGHT. liveness/rest_api.json at head: responseFormat is one row, dead, verifiedAt 2026-09-27, cross-repo, no children, note begins REMOVED 2026-09-27 (#20295); documentation.enabled the same inside the still-drilled documentation. check:liveness exit 0 (87 tombstones graded, state-counts.md current); gen:liveness-counts and gen:strictness-ledger both regenerate to an empty diff (rest_api 12/12/24; api/ 431 sites); check:strictness-ledger exit 0. check:generated currency was read per constituent: locally, check:migration-registry, check:spec-changes, check:upgrade-guide, check:declaration-map (2829 names, current), check:liveness, check:strictness-ledger and the two gen: regenerations above, all exit 0; off CI at this head, the Type Check · source gates job log (job 108741529358, success) shows check:generated --reconcile-only, check:skill-docs, check:meta-url-spelling, check:spec-changes ("up to date"), check:upgrade-guide ("up to date"), check:export-origins ("current: 5225 exports"), check:authorable-surface, check:docs (import examples resolve), check:skill-refs and check:react-blocks with no ##[error]; check:api-surface and turbo run typecheck (which carries check:test-typecheck, the holder of the two @ts-expect-error pins) live in Type Check · consumer gates and Type Check · workspace, both success; check:type-check-debt in Type Check · debt ledger, success; check:liveness / check:strictness-ledger in Spec property liveness, success.

  6. Pins bite — RIGHT. Ablation through scripts/ablation-replace.mjs, held under os-verify-lock.sh (waited 83 s, held 13 s): responseFormat: retiredKey( → responseFormat: z.any().optional() ?? retiredKey( in rest-server.zod.ts (anchor 1 → 0, blob 32ebfe55 → bfbb9755), running vitest run --project repo src/api/rest-api-config-dead-keys-retirement.test.ts: 6 failed | 10 passed — the four responseFormat door cases, the whole-config door, and the tsc pin's parse leg; the documentation.enabled pins, controls and the tree-scoped absence pin stayed green, as they must. Restore proven twice: the tool's own blob == HEAD and my independent git hash-object = 32ebfe55ec10943f865eed81f7a1dbbaa2170a31 = HEAD:…, git diff HEAD and git status --porcelain empty; control before and after the mutation 16/16. The tree-scoped absence pin is the third describe of that file: structural matcher (an object literal that is the value of responseFormat carrying a retired member, or of documentation carrying enabled), five roots with anti-vacuity (more than 1000 files visited, more than 5 of them spelling responseFormat), the two kit test files excluded by name; it is listed in packages/spec/vitest.repo-tests.json and the repo project selected it (the ablation run above is that project). check:cross-package-test-inputs exit 0 — the radius was already declared.

  7. Changeset / semver — RIGHT. @objectstack/spec and @objectstack/rest minor; **BREAKING** banner with FROM → TO and a one-line fix; Clause-②: no (narrowing); the ADR-0087 disposition marker as an HTML comment reading adr-0087: registered rest-api-config-dead-keys-retired. node scripts/check-changeset-no-major.mjs --base a78f731a exit 0; node scripts/check-adr-0087-registration.mjs --base a78f731a exit 0, reading [BREAKING+bang+clause-②-narrowing] registered rest-api-config-dead-keys-retired (new here). Every sentence checked against a measurement: the FROM/TO construction message matches parseDeclaredSubConfig's format; the FROM parse({ documentation: { enabled: false, title } }) output matches base ({ enabled: false, title }); "no reader … no author" matches ①; "every live key parses exactly as before" and "a documentation block no longer grows enabled: true" match ②; the 14 → 12 count matches state-counts.md; "17.5.0" is the next minor from 17.4.0 and the spelling of the 13 sibling tombstones on the same release. The PR body's line and Acceptance-notes bullet agree with the changeset.

  8. CI at the head — RIGHT. Waited until every run completed (2026-09-28T00:48Z): 46 check runs on 5af9ff5a, 39 success, 7 skipped, 0 failure, 0 cancelled. The skips are all expected: Check PR Size and Auto Label skipped on the two secondary workflow runs (they ran and passed on the primary), Packed-tarball smoke (opt-in) twice (opt-in), and Console Pin Gate (the objectui pin did not move). Green includes Build Core, Test Core 1/6–6/6 (the @objectstack/rest local project, in whose population vitest list --filesOnly places rest-api-config-dead-keys-refused.test.ts, and the @objectstack/spec repo project that lists rest-api-config-dead-keys-retirement.test.ts), Type Check · source gates / workspace / consumer gates / debt ledger, Lint & Repo Gates, Spec property liveness, Check Changeset (three runs), Dogfood Regression Gate 1–3/3, Dogfood Verify CLI, Temporal Conformance, Build Docs, Check Documentation Links, Governed Surface Queue Guard, and the four claim/single-writer guards. Vercel status: success. No red to name.

② Semver level

minor + declared breaking is right. The change narrows a published accept set (RestApiConfigSchema refuses two keys it accepted) and widens nothing — no export is added and no accept set grows; the ADR-0087 registrations are ledger data, as #20227 and #20238 declared them. Clause-②: no (narrowing) is the measured value and supersedes the claim's yes; the gates read the same disposition either way. patch would be wrong (a refusal of previously accepted input); major is refused by check-changeset-no-major in the launch window, and the breaking-ness is carried by the banner and the ADR-0087 marker.

③ Boundary flags

  • Pending .changeset/14640-rest-api-liveness-ledger.md (another card, patch) closes with "documentation and responseFormat are accepted, validated and normalized, and change nothing" and counts "14 dead". Compiled next to this changeset, the next @objectstack/spec CHANGELOG will carry both: a Minor entry saying the two keys are refused and a Patch entry below it saying they are accepted and change nothing. A reader who reads only the patch entry would misread the current behaviour; one who reads the release top-down meets the retirement first, and the ledger entry is a dated seeding note. Not blocking; the release compiler should either amend that sentence to past tense or add a one-line pointer to the retirement.
  • RestApiPluginConfigSchema.responseEnvelope (plugin-rest-api.zod.ts) is a second declared envelope toggle with no runtime parser and no liveness enrolment; it is out of this card's scope and was not measured for reach here. Carrier: none.
  • The :4167 hunk is the parseDeclaredApiConfig docblock, a few lines above the "about :4230" the order gave for the normalizeConfig region; it is comment-only, contiguous with the region, and fix(runtime,rest): the dispatcher /meta list prunes what RestServer's list prunes, through one list gate (#20237) #20319 has landed, so the fence's purpose is met.
  • packages/rest/CHANGELOG.md, named in the card's file surface, is release-owned and untouched — correct.

Implemented-by: claude/issue-20295-rest-api-retire
Reviewed-by: session_01Rjy9MeetSfq34PKn81CRiN

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Seat verification: base-merge round · head f57d2e433c375860ac518733e2d642ca6e5b3f07 adopts the at-tier record 5861434720 · 2026-09-28T01:03Z

domain:spec seat 1 (session_01Rjy9MeetSfq34PKn81CRiN). The at-tier review PASSED at 5af9ff5a (record 5861434720). This round only brings main in and regenerates, so the seat verifies that the increment is unchanged instead of re-reviewing the contract.

Why the round was needed. PR #20328 landed as 826f3279 and moved the total row of packages/spec/liveness/state-counts.md. A driverless merge of 826f3279 into 5af9ff5a conflicted on that row, and the queue merges as text.

What the round is

Verified by the seat

  • Three-dot increment. git diff --stat of 5af9ff5a against its merge-base and of f57d2e43 against origin/main are byte-identical: 19 files, +1002/−108, per-file counts equal.
  • Per-file identity. For the 17 non-generated files, the added and removed lines of each file's increment hash identically at both heads. registry.ts and state-counts.md differ from 5af9ff5a only by main's own entries and rows.
  • Driverless merge-tree onto current main c74de10a (a bare clone with no os-regen driver): exit 0. The merged tree adds exactly the PR's 19 files, +1002/−108, and its liveness and registry files equal the PR head's.
  • The dev's checks at f57d2e43, exit 0:
    • check:liveness;
    • check:migration-registry (299 semantic / 219 retired-key / 199 retired-def);
    • check:generated, 15/15 after a locked spec build;
    • check-adr-0087-registration.

The contract judged in 5861434720 is unchanged at this head. The PR lands once CI at f57d2e43 is green or shows only expected skips.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec(api): retire api.responseFormat and api.documentation.enabled (4 keys); the envelope is fixed and enableOpenApi already decides the document

2 participants