From 67bd951eba0680897a39db4b16b6a5c36276effd Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 11:40:50 +0000 Subject: [PATCH 1/2] refactor(rest): drop the `(p as any)` cast at the `publishMetaItem` call site `MetadataProtocol` now declares `publishMetaItem` as an optional member carrying `PublishMetaItemRequest` (#11006, maintainer ruling 2026-08-22, option B), so the cast that was feature-detecting an ADR-0076 D9 server-only extension has nothing left to carry. The request literal is typed through the #9741 `TransportScopedMetaRequest` wrapper, which is the point: an undeclared key at this call site is now a compile error instead of an unchecked payload member. The docblock that existed only to explain why the cast had to stay is replaced rather than left behind. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01VK8rFDtg8eREaxBGX99Csn --- packages/rest/src/rest-server.ts | 60 ++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/packages/rest/src/rest-server.ts b/packages/rest/src/rest-server.ts index 95c26ffd28..1819c6682e 100644 --- a/packages/rest/src/rest-server.ts +++ b/packages/rest/src/rest-server.ts @@ -68,6 +68,7 @@ import type { GetMetaItemRequest, GetMetaItemCachedRequest, GetMetaItemLayeredRequest, + PublishMetaItemRequest, } from '@objectstack/spec/api'; // [#8073] The closed ADR-0112 error vocabulary, so the explain family's single // refusal emitter types its `code` parameter as the vocabulary rather than as @@ -5937,7 +5938,7 @@ export class RestServer { return; } const p = await this.resolveProtocol(environmentId, req); - if (!(p as any).publishMetaItem) { + if (!p.publishMetaItem) { res.status(501).json({ error: 'Publish operation not supported by protocol implementation', }); @@ -6021,34 +6022,38 @@ export class RestServer { // org-scope comment for the measurement. canonicalMetaUrlType(req.params.type), ctx?.tenantId, ); - // [#10350] The cast stays, and what it is load-bearing - // FOR is worth stating so this is not re-filed as a missing - // contract. It is NOT hiding the request shape: measured by - // deleting it and running `pnpm --filter @objectstack/rest - // typecheck`, the compiler answers + // [#11145] The `(p as any)` cast this call carried came off + // when `MetadataProtocol` declared `publishMetaItem` (#11006, + // maintainer ruling 2026-08-22, option B). What the cast was + // load-bearing FOR is recorded because it is counter-intuitive + // and was measured, not assumed: deleting it while the member + // was undeclared answered // `TS2339: Property 'publishMetaItem' does not exist on // type 'RestProtocol'` - // — not a TS2353 about an unknown key. `publishMetaItem` is - // an ADR-0076 D9 SERVER-ONLY extension: `RestProtocol` is - // `DataProtocol & MetadataProtocol`, and `MetadataProtocol` - // (`packages/spec`) declares no such member — only - // `PublishMetaItemResponseSchema` (#7294) exists there, with - // no request schema and no interface entry. So the cast is - // feature detection, exactly like the `auditMetaItem` twin - // a few hundred lines up, and the same measurement holds - // AFTER #10350 declared `packageId` on the implementation's - // request type: that type lives in - // `@objectstack/metadata-protocol`, which `packages/rest` - // deliberately does not depend on. + // — NOT a `TS2353` about an unknown key. The cast was feature + // detection for an ADR-0076 D9 server-only extension, so only + // declaring the member could retire it; widening the + // implementation's own request type in + // `@objectstack/metadata-protocol` (which this package + // deliberately does not depend on) never could, and #10350 + // measured exactly that. // - // Removing it therefore needs `MetadataProtocol` to declare - // the member (plus a `PublishMetaItemRequest` to hang the - // #9741 `TransportScopedMetaRequest` typing off) — a - // `packages/spec` contract decision, promoting an undeclared - // optional extension into a declared one, which is the same - // call the 501 refusal above declines to pre-empt. Filed - // rather than taken here. - const result = await (p as any).publishMetaItem({ + // What replaces it is the point of the exercise, not a + // side effect: the literal below is compiled against the spec + // contract through the #9741 `TransportScopedMetaRequest` + // wrapper, so an undeclared key here is a COMPILE ERROR + // (`TS2353`, measured) instead of a payload member no contract + // has ever seen. `environmentId` is the transport-level + // routing key that wrapper layers on — ⛔ never a protocol + // key; a key that belongs on the request belongs in the spec + // schema. + // + // The 501 feature-detection guard above STAYS. The member is + // declared OPTIONAL (ADR-0076 D9 promotion is additive to a + // shipped contract, and a kernel may not implement the + // promotion door at all), and that same guard is what narrows + // it to callable here. + const publishRequest: TransportScopedMetaRequest = { type: req.params.type, name: req.params.name, organizationId, @@ -6056,7 +6061,8 @@ export class RestServer { ...(actor ? { actor } : {}), ...(message ? { message } : {}), ...(packageId ? { packageId } : {}), - }); + }; + const result = await p.publishMetaItem(publishRequest); res.json(result); } catch (error: any) { handleRouteError(res, error); From a5d4f97b92fae1e80539203e778608fca75be37d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 11:52:27 +0000 Subject: [PATCH 2/2] chore(changeset): patch @objectstack/rest for the retired publishMetaItem cast Part of #11145 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01VK8rFDtg8eREaxBGX99Csn --- .../rest-publish-meta-item-cast-retired.md | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .changeset/rest-publish-meta-item-cast-retired.md diff --git a/.changeset/rest-publish-meta-item-cast-retired.md b/.changeset/rest-publish-meta-item-cast-retired.md new file mode 100644 index 0000000000..24fd68916c --- /dev/null +++ b/.changeset/rest-publish-meta-item-cast-retired.md @@ -0,0 +1,40 @@ +--- +"@objectstack/rest": patch +--- + +refactor(rest): the `publishMetaItem` call site is compiled against the declared contract (#11145) + +The `POST /meta/:type/:name/publish` door in `packages/rest/src/rest-server.ts` +reached its protocol method through `(p as any).publishMetaItem` — once for the +501 feature-detection guard, once for the call — so the compiler checked nothing +about the request literal it built. The cast was load-bearing on **member +existence**, not on request shape: #10350 measured that deleting it answered +`TS2339: Property 'publishMetaItem' does not exist on type 'RestProtocol'`, not +a `TS2353` about an unknown key. `publishMetaItem` was an ADR-0076 D9 +server-only extension, so no amount of widening the implementation's own +parameter type in `@objectstack/metadata-protocol` (which this package +deliberately does not depend on) could have retired it. + +#11006 (maintainer ruling 2026-08-22, option B) declared the member on +`MetadataProtocol` with a `PublishMetaItemRequest`, which is what removes the +prop. The guard is now `if (!p.publishMetaItem)` and the request is a named +const typed `TransportScopedMetaRequest` — the same +shape #9741 gave the meta-read doors and #9805 gave the non-door helpers. + +**No behaviour change of any kind, and nothing about the wire moves.** The +outgoing payload is byte-identical (same keys, same conditional spreads); the +edit hoists the literal into a const and drops a type-level cast. Two things +deliberately survive: + +- the 501 feature-detection guard, because the declared member is **optional** + (a kernel may not implement the promotion door at all) — and it is also what + narrows the member to callable at the call site; +- the transport-level `environmentId`, which stays layered on by the + `TransportScopedMetaRequest` envelope rather than becoming a protocol key, per + the #9741 ruling (2026-08-18). + +What the typing buys, measured rather than asserted: an undeclared key in this +request literal is now `TS2353` at compile time instead of a payload member no +contract has ever seen. The docblock that existed only to explain why the cast +had to stay is replaced rather than left behind — a rationale for a prop that no +longer exists is a declaration that outlived its subject.