feat(spec,rest)!: the served OpenAPI info carries the publisher's api.documentation identity; api.documentation.version retired - #20512
Conversation
….version tombstoned Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH Co-authored-by: Claude <noreply@anthropic.com>
…enAPI info; pins Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH Co-authored-by: Claude <noreply@anthropic.com>
…ion row REMOVED; reference docs; changeset Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH Co-authored-by: Claude <noreply@anthropic.com>
…on names Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH Co-authored-by: Claude <noreply@anthropic.com>
…enapi-info-overlay
📓 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 — 139 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 6a9004d691727794fdf9672010e1589148233f92 && git checkout 6a9004d691727794fdf9672010e1589148233f92
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0bbe4005e82fce2058238720cac7ba5cd2f182d5 8e26fbbe887a339ca71d602db5dc46171281a2e2 && git checkout -B drift-repro 0bbe4005e82fce2058238720cac7ba5cd2f182d5 && git merge --no-ff 8e26fbbe887a339ca71d602db5dc46171281a2e2
node scripts/docs-audit/affected-docs.mjs --json 0bbe4005e82fce2058238720cac7ba5cd2f182d5
|
Contract reviewServed-tier: Inputs: card #20294 (body and all nine comments — triage ① Derived judgmentsAccept set (
Public surface (the served OpenAPI document, Retirement route (skill §2–§4), against the The diff implies no accept-set or surface change beyond these; ② Semver level
Matches what the diff publishes: spec refuses a formerly accepted input ( Clause-②: ③ Boundary flags
Implemented-by: VERDICT: PASS |
…enapi-info-overlay
Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH Co-authored-by: Claude <noreply@anthropic.com>
|
Regen-provenance: 5878749194 ·
Seat's check of the committed trees:
|
Fixes #20294
Fixes #20359
Clause-②: yes (narrowing)
Summary
Executes maintainer ruling B on #20359 (
5862477514, batch #231 item 1, maintainer 「同意 B」) for therest-api-documentationfamily of the #18900 census, under ADR-0049 enforce-or-remove:RestServerConfig.api.documentation. These aretitle,description,termsOfService,contact.name/contact.url/contact.emailandlicense.name/license.url. An authored value now overlays the served OpenAPIinfoon both doors,{apiPath}/openapi.jsonand the environment-scoped twin. With nothing authored, the servedinfois byte-identical to the artifact.documentation.version. The servedinfo.versionstays the spec package version (SPEC_VERSION, written bybuild-openapi.ts), as the [finding] the served openapi.json overrides theinfo.versionthatpackages/specowns, so the published artifact says17.2.0and the served document saysv1#11646 ruling (5405070350) set it. An authoredversionis now refused by aretiredKey()tombstone. Its message names the protocol version as the one source and points an app's own release number todescription.The ruling's execution parameters, each applied:
titleis now.optional(). The materialized'ObjectStack API'default was never served; the artifact's'ObjectStack REST API'stays the default.contactandlicensereplace the bundled object whole, never member by member.registerOpenApiEndpointsand applies on both doors.api.versionand the runtime version never enterinfo.version.documentation.versionhas an ADR-0087 D3 entry (familyrest-api-documentation).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
RestServerplusregisterRoutes()with project scoping on, then drove bothopenapi.jsonhandlers:documentationinfo(both doors)info{}{ title: 'ObjectStack API' }(the materialized default)versionincluded{ description }onlytitle{ license: { name: 'MIT' } }titleThe scoped door answered the same
infoas the unscoped one in every case. The handler's clone is shallow, soenriched.infois 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
GET /api/v1/openapi.jsonandGET /api/v1/environments/:environmentId/openapi.jsoninfoinfo.versionis still the spec package versionrest-openapi-info-overlay.test.ts(1)documentation: {}infoinfo, byte-identicalrest-openapi-route.test.tsis unedited and kept as the no-config controltitle,description,termsOfService,contact,license)infolicense: { name: 'MIT' }{ name: 'MIT' }, nourlcontact: { email }{ email }onlyversion: 'v9'plus an overlay, withOS_RUNTIME_VERSIONset to a sentinelinfo.versionis still the artifact's; no sentinel anywhere ininfoRestApiConfigSchema/RestServerConfigSchemadocumentation.version('2.3.0','17.4.0','')invalid_typeat['documentation', 'version']with the prescriptionpackages/spec/src/api/rest-api-config-dead-keys-retirement.test.ts(3)new RestServer(…)andcreateRestApiPlugin(…).startdocumentation.versionapi.documentation.version, namesRestApiConfigSchemaand carries the prescriptionpackages/rest/src/rest-api-config-dead-keys-refused.test.ts(3)RestApiConfig(input)documentation.versionstringnever: a tsc error at the authoring site@ts-expect-errorpin, held bycheck:test-typecheckRestApiConfigSchema.parse({ documentation: {} }){ title: 'ObjectStack API' }{}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:docsprefixes with[REMOVED]:"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 servedinfo.version(the package's17.4.0), and the ruling means the latter.The kit
packages/spec/src/api/rest-server.zod.ts, thedocumentationblock only:titleis.optional().infofield.versionis aretiredKey()tombstone with an in-schema comment, next to theenabledtombstone. The inline object is a non-strictz.object(), so a bare deletion would strip the key in silence.packages/rest/src/rest-server.ts, only theregisterOpenApiEndpointsregion (three hunks between the discovery registrar andloadOpenApiSpec):private static overlayDocumentationInfo(bundled, documentation)sits directly above the method. It returnsbundleditself when nothing is authored and a new object otherwise, with a closed member list: no spread of the block.enriched.info = RestServer.overlayDocumentationInfo(…), runs beforeres.json. The same closure serves both doors.info.versionthatpackages/specowns, so the published artifact says17.2.0and the served document saysv1#11646 comment there is rewritten as "unset = the artifact's; authored = the publisher's;versionnever".RETIRED_KEYS_BY_MAJOR[18]gainsapi/RestApiConfig:documentation.version(entries/retired-keys/18.api__RestApiConfig__documentation.version.ts).rest-api-documentation-version-retired(entries/semantic/18.rest-api-documentation-version-retired.ts).registry.tsis regenerated bygen:migration-registry.RestServerConfigis plugin TS configuration, never a stack collection member or a stored row. This matches therest-api-config-dead-keys-retiredprecedent on this same block.MIGRATIONS_BY_MAJOR[18].conversionIds.packages/spec/liveness/rest_api.json:title,description,termsOfService, thecontact/licensecontainers and their five members flip tolive. Evidence ispackages/rest/src/rest-server.ts#overlayDocumentationInfoplus the pin file; the producer isnormalizeConfig.versionrow staysdeadwith a REMOVED note._noteaddendum records the change.state-counts.mdmovesrest_apifrom 12 live / 12 dead to 20 / 4. The README's hand-writtenrest_apirow is edited to match.content/docs/references/api/rest-server.mdx.packages/rest/src/rest-openapi-info-overlay.test.ts.[#20294]block inrest-api-config-dead-keys-refused.test.ts.documentation.versionblock in the spec retirement test. Its tree-scoped absence walk now also matches adocumentationobject carryingversion, with anti-vacuity cases in.ts,.jsonand.yaml, plus two neighbour cases that must not match: the route identifierapi.version, and the enforced members alone.packages/spec/src/api/rest-server.test.tsthat authoreddocumentation.version: '1.0.0'drop it.rest-api-config-defaults-follow-spec.pin.test.ts..changeset/20294-openapi-info-publisher-overlay.md:@objectstack/specminorand@objectstack/restminor(both in thefixedgroup).Clause-②: yes (narrowing), FROM → TO with the one-line fix.registered rest-api-documentation-version-retired, in the changeset body wherecheck:adr-0087-registrationreads it.On the
Clause-②spelling. The value is the ruling'syes: a behaviour change on a public document, which now publishes owner-settermsOfService/contact.emailit never carried. The(narrowing)arm is required because the retirement refuses a formerly accepted input.check-adr-0087-registration.mjs#breakingDeclarationreads that arm, as well as the**BREAKING**banner, as a breaking declaration. Theadr-0087:marker lives in the changeset, not in this body. This body carries only the declaration line thatCheck Changesetreads.Verification, at HEAD
69b4d20369b4d203isorigin/main2b24b8b8merged in throughscripts/pm/os-regen-merge.sh. No generated path needed regeneration after the merge:check:generatedread all 15 artifacts current. After the merge I ranpnpm install --frozen-lockfileand rebuilt the rest dependency closure (pnpm --filter '@objectstack/rest^...' build, which includes spec). Every heavy run went throughscripts/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 typecheckandpnpm --filter @objectstack/spec run typecheck: both exit 0. Each istsc --noEmitpluscheck:test-typecheck; spec also runscheck:scripts-typecheck. A greencheck:test-typecheckis what proves the new@ts-expect-errorpin 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.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 115 commands at this head. The--ranreconciliation reads: 115 derived — 113 run, 0 NOT-MEASURED, 2 UNRUN. Of the 113, 112 exit 0 and 1 exits 1:pnpm check:platform-checklistexits 1 withareas/identity-auth.json: ABSENT SYMBOL — packages/plugins/plugin-auth/src/auth-plugin.ts#twoFactor. This red is inherited frommain: this diff touches neitherdocs/qa/**norpackages/plugins/plugin-auth/**(0 diff lines againstorigin/mainthere). The anchor went stale when7d630889turned the literaltwoFactor: trueinto an inlineplugins: { twoFactor: true }object. The gate is deliberately outside per-PR CI (thelint.ymlcomment near the checklist step).check:skill-examplesfirst exited 3 (PREREQUISITE NOT MET: noclient-reactdeclarations). It went green after building@objectstack/clientand@objectstack/client-react: 259 prose examples type-check.pnpm check:dual-build-cjs-loads, which reads every package'sdist/(a whole-tree build is outside this card's scope; CI's Build Core builds it), andpnpm check:type-check-debt, whose--re-measurerebuilds every package. This diff touches no DEBT or EXEMPT package, andcheck:type-check-coverageran with exit 0.eslint --no-inline-config --format jsonover the 11 changed code files: 11 files, 0 errors, 0 warnings. The population comes fromeslint.config.mjs's catch-allfiles: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}'], which all 11 are in. The config never enables type-aware linting (noparserOptions.project, no typed rules, as stated in the config itself), so this diff cannot move the verdict on any untouched file. The repo-widepnpm lintis CI's.turbo ls --affectedwas not used to narrow anything; consumer packages were not rebuilt. The public surface that moves is theRestApiConfiginput type (documentation.versionbecomesnever) and the parsedtitle(string | undefined). The retirement playbook's tree-scoped absence pin is the sweep for authoring sites. It walkspackages,examples,skills,contentandscriptsand finds none. No non-test reader ofdocumentation.titleexists outsidepackages/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 emptygit diff HEAD. Tree status was empty before and after. Each subject is imported from source (./rest-server,./rest-server.zod), so nodist/preflight applies. Every direction observed is the expected one, red.rest-openapi-info-overlay.test.tson the unmutated tree: 11/11 passed.enriched.info = RestServer.overlayDocumentationInfo(…);→ a comment. Blob3f01f03a→5c7dde2c. Result: 9 failed | 2 passed. The two survivors are exactly the nothing-authored controls. Restored (blob == HEAD).return { ...bundled, ...authored };→return Object.assign(bundled ?? {}, authored);. Blob3f01f03a→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.version: retiredKey(→version: z.string().optional() ?? retiredKey(. Blobe69191ec→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.const probe20294: RestApiConfig = { documentation: { title: 'Acme', version: '2.3.0' } }inpackages/rest/srcand ranpnpm --filter @objectstack/rest exec tsc --noEmit. It exited 1 withsrc/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.tsto theregisterOpenApiEndpointsregion, so two comments are left untouched:NormalizedRestServerConfig.api.documentationcomment (near:1116) still says a writtendocumentation"arrives with its OWN declared inner defaults applied (.title)". There are none now.parseDeclaredApiConfigdocblock's [finding]RestServer.normalizeConfigstill discards the parsedapioutput — its??chain duplicatesRestApiConfigSchema's defaults key for key, and the validate-only reason has expired #14366 paragraph (near:4112) saysdocumentationhas "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 rest: the served OpenAPI document'sinfoblock comes fromapi.documentation(9 keys) #20294 madetitleoptional)" to the first and "(until rest: the served OpenAPI document'sinfoblock comes fromapi.documentation(9 keys) #20294's overlay)" to the second. Carrier: whoever next touches either region.Two title literals not covered by this ruling:
/docsviewer page's HTMLtitlestays the literalObjectStack API Docs. It is the browser-tab title of the Scalar page, not aninfofield. The viewer renders the served document, overlaidinfo.titleincluded, in the page body. No declared contract ties the tab toinfo.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 underpackages/**(only migration-entry prose names the schema), against a lit control (RestApiConfigSchemafound inrest-server.ts). It has no runtime parser (packages/restdeclares its ownRestApiPluginConfiginterface) and no ledger enrolment. Not filed; carrier: none.Release-owned files, not edited:
packages/rest/CHANGELOG.mdandpackages/spec/CHANGELOG.md. The changeset is their input. The pending.changeset/14640-rest-api-liveness-ledger.mdstill describesdocumentationas changing nothing, which was true when it landed. The release compiles it next to this changeset.Reachability is unchanged:
RestServerConfigis embedder-only.os serveforwards onlyenableProjectScoping/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
5878320739on #20359. That is the ruling carrier; its ruling5862477514orders this PR to close it.Generated by Claude Code