feat(spec,client): declare the publish door's response contract (#7294) - #7356
Conversation
`POST /api/v1/meta/:type/:name/publish` is served by `@objectstack/rest` (`rest-server.ts` registers it and hands `publishMetaItem`'s return straight to `res.json()`), and had no declaration behind it: `PublishMetaItem` appeared nowhere under `packages/spec/src/`, and the route was absent from `plugin-rest-api.zod.ts`'s metadata table. So `version` on the publish response sat in exactly the state `version` on the save response sat in before #5745 — the ADR-0008 OCC token, echoed back as `If-Match` to get a 409 instead of a lost update, on a public wire surface with nothing declaring it. This carries the #5745 "declared = returned" discipline one door over, with the same three artifacts: - `PublishMetaItemResponseSchema` declares the FULL measured body: `success` / `version` / `seq` required, `message` plus the three conditional side-effect receipts (`seedApplied` / `materializeApplied` / `projectionApplied`) optional. Measured from the producer, not assumed — its single response literal always sets the first three and attaches each receipt only when the matching side effect ran, so an absent receipt means "did not run", never "failed". - The endpoint declaration, matching the five sibling metadata entries. No `requestSchema`: the body's only read key is `message`, taken only when already a string, so the route cannot 400 a malformed body and declaring one would advertise a gate that does not run (#3899). - `packages/objectql/src/publish-meta-response-conformance.test.ts` — the producer-side gate mirroring the save door's, driving a real `publishMetaItem` against a real ObjectQL engine through the schema across the plain shape and every receipt path. Also: - `client.metadata.publishItem()` is typed `Promise<PublishMetaItemResponse>` and the type re-exported, matching `saveItem` / `SaveMetaItemResponse`. It resolved to `any` before, for want of a declaration to point at. - `publishMetaItem`'s own `Promise<...>` annotation omitted `projectionApplied` while the body assigned it — the same declared-≠-returned gap one layer down. No behavior change: nothing about the response body moved. Closes #7294 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DuV91PKtmJ8UFUtDjqXy5h
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 3 package(s): 109 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also reference the affected code. These are read-only:
|
Carries the #5745 "declared = returned" discipline one door over: the publish door gets the three artifacts the save door already has.
Premise re-measured on fresh
origin/main(88154be)Every claim on the card re-checked before writing a line — all three still hold:
rest-server.ts:5481registersPOST {metaPath}/:type/:name/publish;:5499awaitspublishMetaItem(...)and:5504hands the result tores.json()verbatimprotocol.ts:9769-9798— one response literal:success/version/seq/message, thenseedApplied/materializeApplied/projectionAppliedattached conditionally byrunPublishSideEffectsgrep -rn "PublishMetaItem" packages/spec/src/→ zero hits;grep -n publish packages/spec/src/api/plugin-rest-api.zod.ts→ zero hitspublishMetaItemis implemented once (ObjectStackProtocolImplementation); every other hit is a caller or a commentrequiredbelowSo
versionon this door sat in exactly the stateversionon the save door sat in before #5745 — the ADR-0008 OCC token, the value a caller echoes back asIf-Matchto get a 409 instead of a lost update, on a public wire surface with nothing declaring it.Changes
packages/spec/src/api/protocol.zod.tsPublishMetaItemResponseSchema— the full measured body, modelled onSaveMetaItemResponseSchema(style,describe()density, OCC framing).success/version/seqrequired;message+ the three receipts optional. Plus thePublishMetaItemResponsealias.packages/spec/src/api/plugin-rest-api.zod.tsPOST /:type/:name/publish,handler: 'publishMetaItem',category: 'metadata',responseSchema: 'PublishMetaItemResponseSchema',permissions: ['metadata.write'], field-for-field with the five siblings.publishMetaItemadded to the group'smethods.packages/objectql/src/publish-meta-response-conformance.test.tssave-meta-response-conformance.test.tsincluding its header discipline. 9 cases.packages/spec/src/api/protocol.test.tspackages/client/src/index.tspublishItem()typedPromise<PublishMetaItemResponse>(wasany); type re-exported.packages/metadata-protocol/src/protocol.tspublishMetaItem'sPromise<...>annotation gainsprojectionApplied— it was assigned in the body and missing from the type.packages/spec/src/type-alias-convention.pin.test.tsIso836(isomorphic: no.default()/.transform()/.catch()/.pipe()anywhere in the tree), count 824 → 825 + both prose statements.packages/spec/src/api/plugin-rest-api.test.tsTwo judgement calls, stated
messageis optional, not required — even though the producer sets it unconditionally today. Its own signature declaresmessage?, theSaveMetaItemResponsetwin declares it optional, and the failure #5745 exists to prevent is stripping: an over-tightrequiredwould make a real response fail its own contract, while an optional field that is always present strips nothing.success/version/seqare required — the sole producer's only success return always sets them, and the gate below carries the tripwire for that decision.No
requestSchema(#3899): the body is optional and its only read key ismessage, taken only when it is already a string and ignored otherwise. The route cannot 400 a malformed body, so declaring a request schema would advertise a gate that does not run.packages/clientre-export: includedThe mirror is exact and cheap.
publishItem()already existed and already returnedunwrapResponse<any>— the concrete symptom the issue named ("PublishMetaItemResponsecannot be named at the type level the waySaveMetaItemResponsecan"). Typing it pulls in no new surface: one type import, one re-export line, both beside theSaveMetaItemResponseones. Client suite green (279/279);tsc --noEmitclean.Conformance-gate probe — direction predicted first
Prediction, stated before running: delete the
seedAppliedblock from the schema and the producer gate must go RED in exactly 2 cases (the two seed paths) via non-emptystrippedKeys, the other 7 stay green, and the save-door gate is untouched.Result — matched exactly:
seedAppliedremoved from schemapublish-meta-response-conformance.test.tsAssertionError: expected [ 'seedApplied' ] to deeply equal [], at the twostrippedKeys(raw)lines; other 7 greenprotocol.test.ts(spec-side)seedAppliedcasessave-meta-response-conformance.test.tsOne result stronger than predicted:
pnpm --filter @objectstack/spec builditself refuses the removal before a test can run —so the declaration is now covered by the authorable-surface gate too, not only by the two suites. (The probe was completed against a
tsup-only build to reach the test layer, then reverted and rebuilt; the schema file is byte-identical to its pre-probe state.)Gates
check:generated(all 11 artifacts)pnpm --filter @objectstack/spec buildgen:api-surfacerefused once mid-run for exactly that reason and was re-run after a rebuild)turbo typecheck(spec, client, objectql, metadata-protocol, rest + deps)check:route-envelope,check:doc-authoring,check:quick-reference-counts,check:nul-bytesDual-snapshot rule, in full — the new public exports land in both, regenerated after a real build:
packages/spec/api-surface/api.json(PublishMetaItemResponse (type),PublishMetaItemResponseSchema (const)) andpackages/spec/export-origins/api.json(both, resolved tosrc/api/protocol.zod.ts). Docs reference rows came through the gen chain (content/docs/references/api/protocol.mdx+index.mdx, 1575 → 1576 schemas), along withauthorable-surface/api.json,json-schema.manifest/api.jsonand the strictness-ledger counts.One non-major changeset (
spec: minor,metadata-protocol+client: patch). Nocontent/docs/releases/ordocs/adr/**touched.Closes #7294
Generated by Claude Code