From d439bc405d3df1971135b5064e24eef8bae4c36a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 22:29:22 +0000 Subject: [PATCH 1/3] fix(spec): declare MetadataProtocol.getMetaItemLayered; drop the dead 'overlay' lockSource arm Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016D9wdJR14KKCxz1WgdAzcw --- packages/metadata-protocol/src/protocol.ts | 10 +++++++++- packages/spec/src/api/protocol.zod.ts | 22 +++++++++++----------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/packages/metadata-protocol/src/protocol.ts b/packages/metadata-protocol/src/protocol.ts index 7a0c4673b8..3028c0b3ca 100644 --- a/packages/metadata-protocol/src/protocol.ts +++ b/packages/metadata-protocol/src/protocol.ts @@ -73,6 +73,7 @@ import { evaluateLockForDelete, resolveLockState, type MetadataLock, + type MetadataLockSource, type MetadataProvenance, } from '@objectstack/spec/kernel'; import { validateObjectNamespacePrefix, deriveNamespaceFromPackageId } from '@objectstack/spec/kernel'; @@ -6081,7 +6082,14 @@ export class ObjectStackProtocolImplementation implements // ── ADR-0010 protection envelope ── lock: MetadataLock; lockReason?: string; - lockSource?: 'artifact' | 'package' | 'env-forced' | 'overlay'; + // `MetadataLockSource` (artifact | package | env-forced) — the only + // producer feeding this field on this path is `resolveLockState`, + // whose return is typed `MetadataLockSource | undefined`. The + // `'overlay'` arm this annotation used to carry was dead: the one + // `lockSource: 'overlay'` producer in this file belongs to + // `getEffectiveLock`, a write/delete-door helper that never feeds + // this response (#9740). + lockSource?: MetadataLockSource; lockDocsUrl?: string; provenance?: MetadataProvenance; packageId?: string; diff --git a/packages/spec/src/api/protocol.zod.ts b/packages/spec/src/api/protocol.zod.ts index 039a5644ce..7f54844708 100644 --- a/packages/spec/src/api/protocol.zod.ts +++ b/packages/spec/src/api/protocol.zod.ts @@ -2337,17 +2337,17 @@ export interface MetadataProtocol { */ deleteMetaItem?(request: DeleteMetaItemRequest): Promise; getMetaItemCached?(request: GetMetaItemCachedRequest): Promise; - // `getMetaItemLayered` is deliberately NOT declared here yet, although both - // its request and response schemas are (see them above). Declaring the - // member makes tsc check `metadata-protocol`'s implementation against - // `GetMetaItemLayeredResponse`, and that check currently fails on one - // member: the implementation's inline return type annotates `lockSource` - // with an `'overlay'` arm that its only producer (`resolveLockState`, whose - // return is typed `MetadataLockSource | undefined`) can never emit — an - // over-wide annotation, not enforced behaviour. Until that annotation is - // corrected at the implementation, adding the member here would either lie - // about the wire enum or hard-break the implementing class. Tracked as its - // own card (filed from #9726). + /** + * Three-layer diagnostic read (`GET /api/v1/meta/:type/:name/layers`) — + * packaged baseline, tenant overlay row and merged result side by side; see + * {@link GetMetaItemLayeredResponseSchema} for the shape and its #5882 + * route-separation rationale. A declared-surface catch-up, not a new + * capability: `metadata-protocol` has shipped this verb since the layered + * route landed. Declared optional like its `getMetaItemCached` / + * `deleteMetaItem` siblings — additive to a shipped contract, with the + * implementation predating the declaration. + */ + getMetaItemLayered?(request: GetMetaItemLayeredRequest): Promise; getUiView?(request: GetUiViewRequest): Promise; } From 4a565ea3b9a03890de86fb1c9aa9e45cdb962eec Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 22:43:00 +0000 Subject: [PATCH 2/3] test(spec): pin MetadataProtocol.getMetaItemLayered's declaration and the closed lockSource vocabulary Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016D9wdJR14KKCxz1WgdAzcw --- packages/spec/src/api/protocol.test.ts | 34 ++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/packages/spec/src/api/protocol.test.ts b/packages/spec/src/api/protocol.test.ts index 0bb6034ff6..2f317506c4 100644 --- a/packages/spec/src/api/protocol.test.ts +++ b/packages/spec/src/api/protocol.test.ts @@ -59,6 +59,12 @@ import { GetFieldLabelsResponseSchema, } from './protocol.zod'; import type { ListNotificationsRequest } from './protocol.zod'; +import { expectTypeOf } from 'vitest'; +import type { + MetadataProtocol, + GetMetaItemLayeredRequest, + GetMetaItemLayeredResponse, +} from './protocol.zod'; describe('ObjectStack Protocol', () => { @@ -1350,3 +1356,31 @@ describe('meta-read request schemas declare organizationId (#9726 — declared = expect(GetMetaItemLayeredRequestSchema.safeParse({ name: 'account_list' }).success).toBe(false); }); }); + +describe('MetadataProtocol declares getMetaItemLayered (#9740)', () => { + // Type-level pins (compiled by the spec test typecheck, the + // translation-typegen.test.ts pattern). The member is an interface + // declaration with no runtime shadow, so its presence and shape are only + // observable to tsc — these pins are what turns red if the member is + // dropped again or drifts off the layered schemas. + + it('declares the member optional, against the layered request/response schemas', () => { + // Optional like its `getMetaItemCached` / `deleteMetaItem` siblings: + // additive to a shipped contract, implementation predating declaration. + expectTypeOf().toEqualTypeOf< + ((request: GetMetaItemLayeredRequest) => Promise) | undefined + >(); + }); + + it("keeps the layered lockSource vocabulary closed — no 'overlay' arm", () => { + // The drift this card closed: the implementation's inline annotation + // carried an 'overlay' arm no producer on the layered read path can emit + // (the only `lockSource: 'overlay'` producer is `getEffectiveLock`, a + // write/delete-door helper). The declared wire enum stays + // artifact | package | env-forced; widening it without a producer is the + // declared-but-unenforced direction Prime Directive #10 forbids. + expectTypeOf().toEqualTypeOf< + 'artifact' | 'package' | 'env-forced' | undefined + >(); + }); +}); From e1ed96c9fb920f74ba0dab2649e07701f2996c81 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 22:43:47 +0000 Subject: [PATCH 3/3] chore: changeset for #9740 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016D9wdJR14KKCxz1WgdAzcw --- .changeset/layered-interface-member.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/layered-interface-member.md diff --git a/.changeset/layered-interface-member.md b/.changeset/layered-interface-member.md new file mode 100644 index 0000000000..efcc902ddc --- /dev/null +++ b/.changeset/layered-interface-member.md @@ -0,0 +1,8 @@ +--- +"@objectstack/spec": minor +"@objectstack/metadata-protocol": patch +--- + +Declare `MetadataProtocol.getMetaItemLayered` — the layered three-way diagnostic read (`GET /api/v1/meta/:type/:name/layers`) now appears on the protocol interface, typed against the already-declared `GetMetaItemLayeredRequestSchema` / `GetMetaItemLayeredResponseSchema`, so callers no longer reach the verb through `any`. Declared optional like its `getMetaItemCached` / `deleteMetaItem` siblings: a declared-surface catch-up to a shipped verb, not a new capability. + +In `@objectstack/metadata-protocol`, the implementation's inline return-type annotation for `getMetaItemLayered` drops its dead `'overlay'` arm on `lockSource` and annotates with `MetadataLockSource` directly — the only producer feeding that field on the layered read path is `resolveLockState`, whose return is already typed `MetadataLockSource | undefined` (`'artifact' | 'package' | 'env-forced'`); the `'overlay'` literal in the file belongs to `getEffectiveLock`, a write/delete-door helper that never feeds this response. Type-level change only; no runtime behaviour or wire vocabulary changes.