feat(spec,rest)!: retire api.responseFormat and api.documentation.enabled (ADR-0049) - #20343
objectstack-fleet[bot] wants to merge 10 commits into
Conversation
…(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
…pi retirement 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
… registered 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
📓 Docs Drift CheckThis PR changes 2 package(s): ⛔ 4 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 138 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
…nt widens nothing Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN
Contract reviewServed-tier: 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 ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN
Seat verification: base-merge round · head
|
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): oneretiredKey()tombstone for the container.api.documentation.enabled— a tombstone inside the still-livedocumentationblock. Its other members (title,description,version,termsOfService,contact,license) are untouched: they belong to the sibling card rest: the served OpenAPI document'sinfoblock comes fromapi.documentation(9 keys) #20294.Both keys were parsed, defaulted and copied into
RestServer's config bynormalizeConfig, and nothing read them back.envelope: falseunwrapped no response.documentation.enabled: falseturned no document off, becauseapi.enableOpenApidecides that mount. Now each key carries its prescription at the schema,RestServerconstruction refuses it (so does the REST plugin'sstart), andnormalizeConfigneither forwards nor re-defaults it.Accept / refuse changes — every one pinned
RestApiConfigSchemaresponseFormat: {…}—{ envelope: false }, the old defaults,{ includePagination: false },{}invalid_typeat['responseFormat'], message = prescription belowpackages/spec/src/api/rest-api-config-dead-keys-retirement.test.tsRestServerConfigSchemaapi.responseFormatapi.responseFormat, same prescriptionRestApiConfigSchemadocumentation.enabled: false/true(the old default)true)invalid_typeat['documentation', 'enabled'], prescription belowRestServerConfigSchemaapi.documentation.enabledapi.documentation.enablednew RestServer(…)andcreateRestApiPlugin(…).startapi.responseFormat/api.documentation.enabled,RestApiConfigSchemaand the prescriptionpackages/rest/src/rest-api-config-dead-keys-refused.test.tsRestApiConfigSchemadocumentation: {}{ enabled: true, title }{ title }rest-server.test.ts,rest-config-parse-not-cast.test.ts§DRestApiConfig(input)never(a tsc error at the authoring site)@ts-expect-errorpins, held bycheck:test-typecheckUnchanged, pinned as controls: every live key of the
apiblock parses as before, includingdocumentation's other members; a config without the two keys mounts the same route set (mounted({ documentation: { title, description } })equalsmounted({}));enableOpenApi: falseremoves exactlyGET /api/v1/docsandGET /api/v1/openapi.json.Refusal texts (verbatim; both are also the new
.describe()text, prefixed[REMOVED])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(orapi.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 metasentence in either prescription, on purpose: there is no D2 conversion for the command to list (see below), matching the siblingcrud.*/metadata.*/batch.*tombstones on this same file.Premises measured before editing (origin/main 4e0f72e)
packages/**non-test code: 0 reads — the only code sites wereNormalizedRestServerConfig's type andnormalizeConfig's own write. Lit control on the same instrument:enableOpenApifinds its read atregisterRoutes. A spread / whole-block-destructure sweep overpackages/rest/src(non-test) returns only the parse call andconst { enableProjectScoping, projectResolution } = this.config.api. objectui at its pinf8a9d0fb: 0 forRestApiConfig|RestServerConfig,responseFormat,includePagination,enableOpenApi(controlbasePath= 184). cloud at96eb092: 0 authoring sites for the same terms anddocumentation.enabled(controlcreateRestApiPlugin= 11; every call forwards the stack's own top-levelapi:block, whose schema carries neither key).@examplein the schema docblock; all are converted (the example now showsenableOpenApi). No example app, skill, form, i18n bundle or hand-written doc authors either key.RestApiConfigSchemaand its inlinedocumentationobject are non-strictz.object()s ⇒retiredKey()tombstones (a bare deletion would strip the key in silence, ADR-0104). Ledger rows staydeadwith 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 (thecrud.patternsprecedent).normalizeConfigno longer listsresponseFormat;documentationstill passes through, so the retiredenabledis neither forwarded nor re-defaulted (pinned).Choices settled here (four axes)
responseFormatretired 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 acceptingresponseFormat: {}— 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 (thecrud.patternsprecedent, which also collapsed child rows).crud.patternsposture), it does not.omit()and ignore (therequireAuthposture). 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.@objectstack/specalready declares inscripts/cross-package-test-inputs.mjs; no new declaration was needed (check:cross-package-test-inputsgreen). Its matcher is structural so that the agent alias map'sresponseFormat: 'structuredOutput'and an OpenAI-styleresponseFormat: { type }never match.The retirement kit
packages/spec/src/api/rest-server.zod.ts— two tombstones with in-schema comments; docblock example converted.packages/rest/src/rest-server.ts— only theNormalizedRestServerConfig.apitype (near the old:1105) and theparseDeclaredApiConfig/normalizeConfigregion. 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.RETIRED_KEYS_BY_MAJOR[18]gainsapi/RestApiConfig:responseFormatandapi/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 aRestServerConfigis plugin TS configuration, never a stack collection member or a stored row. The generated regions ofregistry.tsare regenerated.packages/spec/liveness/rest_api.json(both rows REMOVED,cross-reposcope,_noteaddendum),liveness/README.mdrow, generatedstate-counts.md(rest_api14 → 12 dead, 26 → 24 classified).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 inlineresponseFormatobject left).repoproject); converted cases inrest-server.test.ts,rest-config-parse-not-cast.test.ts§D and one comment inrest-api-config-defaults-follow-spec.pin.test.ts..changeset/20295-rest-api-config-dead-keys-retired.md—@objectstack/specand@objectstack/restminor, BREAKING banner, FROM → TO with the one-line fix, and the ADR-0087 dispositionregistered rest-api-config-dead-keys-retired.Verification — at HEAD
6f07f0c1All heavy runs went through
scripts/pm/os-verify-lock.sh; every exit code below was captured to disk before any pipe.6f07f0c1isorigin/maina78f731amerged in (after #20319 landed) throughscripts/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 typecheckandpnpm --filter @objectstack/rest run typecheck— both exit 0 (tsc --noEmitpluscheck:test-typecheck; the spec one alsocheck:scripts-typecheck).check:test-typecheckgreen is what proves the two@ts-expect-errorpins 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.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 115 commands for this diff at this head;--ranreconciliation: 115 accounted — 113 run, all exit 0; 2 NOT MEASURED; 0 unrun.pnpm check:dual-build-cjs-loads— exit 3, PREREQUISITE NOT MET: it reads every package'sdist/, and a whole-tree build is outside this card's local scope (CI's Build Core builds it).pnpm check:type-check-debt— its--re-measurerebuilds 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-coverageran, exit 0.packages,examples,skills,contentandscriptsand finds noapiblock authoring either key anywhere (anti-vacuity: more than 1000 files visited, more than 5 of them spellresponseFormat). The near consumers that parse or type this config were also run:@objectstack/coresrc/qa/http-adapter.test.ts(it parsesRestApiConfigSchema.parse({})for the route prefix) — exit 0, 32 passed;@objectstack/clientclient.data-prefix.test.ts+client.metadata-prefix.test.ts(they type aRestServerConfig) — 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 emptygit 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.responseFormat: retiredKey(→responseFormat: z.any().optional() ?? retiredKey(inrest-server.zod.ts(anchor 1 → 0, blob32ebfe55→bfbb9755). Spec retirement test: 6 failed | 10 passed — the fourresponseFormatdoor pins, the whole-config door, and the tsc pin's parse leg. Restored (blob == HEAD); control rerun 16/16.ablation-replacerefused (anchor 1 → 1), restored, and never ran the test. Redone with a disjoint spelling:responseFormat: { envelope: false } as never,planted beforeenableDiscovery: true,inpackages/spec/src/api/rest-server.test.ts(anchor 1 → 0, blob794c1326→0766f4e0). 1 failed | 15 passed — the absence pin, namingpackages/spec/src/api/rest-server.test.ts:644. Restored (blob == HEAD).RestApiConfigSchema.omit({ requireAuth: true })→.omit({ requireAuth: true, responseFormat: true })inrest-server.ts— the silent-strip posture (anchor 1 → 0, blobabf25fa0→e93b894a). Rest refusal test: 3 failed | 5 passed — theresponseFormatrefusal, its no-api.versionpositive control and the plugin path; thedocumentation.enabledpins stay green, as they must. Restored (blob == HEAD); control rerun 8/8.After all four legs:
git diff HEADempty, 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 nodist/preflight applies.Acceptance notes
packages/rest/CHANGELOG.mdis 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) saysdocumentationandresponseFormat"are accepted, validated and normalized, and change nothing" — true when it landed; not edited here. The release compiles it next to this changeset.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 underno. On that measurement the seat answeredno (narrowing), and this body and the changeset carry it. The gate readings are unchanged (minor, declared breaking,registered).RestApiPluginConfigSchema.responseEnvelopeinpackages/spec/src/api/plugin-rest-api.zod.tsis a second declared envelope toggle. The spec schema has no runtime parser (packages/restdeclares its ownRestApiPluginConfiginterface) and is not enrolled in any liveness ledger. No reach was measured, so nothing is filed. Carrier: none.docs/audits/**is not on the register).Generated by Claude Code