From a81470ec858f298189e8ef2000195f941ac3c921 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 05:23:51 +0000 Subject: [PATCH 1/4] fix(spec,rest): three routes stop serving shapes their responseSchema never declared (#5882 #5950 #6442) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sweep #6487 — one admission criterion: a route serves a response shape that its declared `responseSchema` does not describe. Three members, one direction each, each stated with its reason rather than picked for cheapness. ## #5950 — `GET /meta/:type/:name` declares the ADR-0010 protection envelope The uncached branch has always sent the protection envelope on top of `{ type, name, item }` — `translateMetaEnvelope` does `{ ...envelope, item }`, so `metadata-protocol`'s `getMetaItem` return reaches the wire verbatim — while `GetMetaItemResponseSchema` declared three keys. `.parse()` therefore STRIPPED every carrier, and `GetMetaItemResponse` could not even name them, so reading `lock` meant a cast: the consumer-side tolerance Prime Directive #12 rejects. `lock` is the read half of the ADR-0008 optimistic-concurrency chain whose write half #5745 declared on `SaveMetaItemResponseSchema`; this is that same gap, read side. Measured against `origin/main` rather than taken from the issue: the envelope is TEN keys, not the three the issue named (it quoted the return statement up to `lockSource` and stopped). Declaring only three would have left seven still undeclared — the member's own criterion unmet. The full set is `lock`, `lockReason`, `lockSource`, `lockDocsUrl`, `provenance`, `packageId`, `packageVersion`, `editable`, `deletable`, `resettable`. All ten are OPTIONAL, and that is measured too. The route reaches a body by two branches that publish different amounts: the cached branch — THE DEFAULT, since `enableCache` defaults to `true` — rebuilds the envelope as `{ type, name, item }` and deliberately never consults the lock resolver, while the uncached branch always sets `lock` / `editable` / `deletable` / `resettable` and sets the other six only when the resolved document carries the matching `_`-prefixed field. So `optional` means "this branch did not publish it", never "unlocked". Declaring them required would make the default deployment's own response fail its own contract — the #5563 defect in mirror image. Zero runtime change. Whether lock presence should depend on a server-side cache setting at all is the larger question #5950 raises explicitly, and it is NOT decided here. ## #5882 — the layered projection gets its own path `?layers=true` made one route answer two unrelated resource representations: the ordinary envelope, and a diagnostic projection showing the packaged baseline, the tenant overlay and the merged result side by side (Studio's "code default vs override vs effective" tabs). The route declared one `responseSchema`, so any client generated from the route table parsed the flagged call wrongly. Per the maintainer's 2026-08-06 ruling: the projection becomes `GET /meta/:type/:name/layers`, declared by a new `GetMetaItemLayeredResponseSchema`. One path, one shape. The rejected alternative was teaching the route declaration to express "two shapes chosen by a flag" — a new primitive every future tool must understand, and conditional response selection is exactly where codegen and AI-written clients go wrong. The declared shape is measured from the producer, and it corrects two claims that were wrong in both the issue body and the in-code comment: the projection carries `_diagnostics`, not `validation`, and it carries the same ADR-0010 protection envelope as the ordinary read, so it is eighteen keys rather than seven. On this path there is exactly ONE producer (the layered view skips the cache), so `lock` / `editable` / `deletable` / `resettable` are guaranteed and declared REQUIRED — the asymmetry with the ordinary read is real and stated. `?layers=` stays for a deprecation window and answers the IDENTICAL body: both entry points call one extracted helper, so the window's promise cannot quietly stop being true. It now carries `Deprecation: true` (RFC 9745) and a `Link` header naming the successor path (RFC 8288) — the same machine-readable pairing `versioning.zod.ts` already describes for retiring API versions. No `Sunset` date: choosing the hard cut-off is a maintainer call, and an invented date is worse than none. The route is registered before `/:type/:section/:name`, which would otherwise capture the path as section=, name="layers" under a first-match router; the ordering is pinned by a test rather than left to reading order. ## #6442 — `GET /analytics/meta` narrows to the shape it serves `AnalyticsMetadataResponseSchema.data` declared `{ cubes: CubeSchema[] }` while `AnalyticsService.getMeta` and its `driver-memory` twin both return a bare `CubeMeta[]` that `runtime/src/domains/analytics.ts` hands to `success()` verbatim. A client written against the published contract read `data.cubes` and got `undefined`; validating a live response against the schema failed outright. `packages/spec` stated both shapes itself — the TS contract in `contracts/analytics-service.ts` already agreed with the runtime, and this schema was the lone outlier. Per the maintainer's 2026-08-08 ruling: narrow the declaration. Zero runtime change. The projection is declared once and bound to the `CubeMeta` interface by an exported compile-time assertion, so the two statements of one shape can no longer drift — which was the root cause. The generated `references/api/analytics.mdx` corrects itself; the hand-written `data-api.mdx:393-395` already described the array form and is untouched. No new `CubeMeta` type alias: `contracts/analytics-service.ts` already owns that name, and a second name for one type is both the ADR-0122 D3 permanent synonym and a new dual-source export. ## Verification Reverse-verified per member, direction predicted before running: dropping the protection mixin turns the uncached assertions red (`lock` reads `undefined` — Zod strips what is undeclared) and the type-level pin red at `tsc`; renaming the new route turns seven layered end-to-end tests red; restoring the wide analytics declaration turns the narrowed parse assertions red. The type-level pins are exported at module scope deliberately: an unread alias in a test body is TS6196, and `packages/spec` compiles its tests through `tsconfig.test.json`, so these go red at `pnpm typecheck` rather than being phantom checks. Fixture triage per case rather than a batch re-spell: the analytics suite's "should reject missing cubes" would have kept passing against the new schema for the WRONG reason (`{}` is not an array either), pinning a `cubes` key that no longer exists, so it is replaced by the assertion that carries the change's actual load — the previously-declared wrapper is now rejected. --- .../undeclared-response-shapes-sweep.md | 54 ++++ ...07-unknown-key-strictness-ledger.counts.md | 2 +- .../rest/src/meta-item-layered-route.test.ts | 236 ++++++++++++++++++ packages/rest/src/rest-server.ts | 139 +++++++++-- packages/spec/authorable-surface/api.json | 34 +++ packages/spec/json-schema.manifest/api.json | 3 + packages/spec/src/api/analytics.test.ts | 147 ++++++++--- packages/spec/src/api/analytics.zod.ts | 83 +++++- .../src/api/meta-item-response-shapes.test.ts | 214 ++++++++++++++++ packages/spec/src/api/plugin-rest-api.test.ts | 9 +- packages/spec/src/api/plugin-rest-api.zod.ts | 26 +- packages/spec/src/api/protocol.zod.ts | 184 ++++++++++++++ .../src/type-alias-convention.pin.test.ts | 25 +- 13 files changed, 1085 insertions(+), 71 deletions(-) create mode 100644 .changeset/undeclared-response-shapes-sweep.md create mode 100644 packages/rest/src/meta-item-layered-route.test.ts create mode 100644 packages/spec/src/api/meta-item-response-shapes.test.ts diff --git a/.changeset/undeclared-response-shapes-sweep.md b/.changeset/undeclared-response-shapes-sweep.md new file mode 100644 index 0000000000..3e6b5bbdb0 --- /dev/null +++ b/.changeset/undeclared-response-shapes-sweep.md @@ -0,0 +1,54 @@ +--- +"@objectstack/spec": minor +"@objectstack/rest": minor +--- + +fix(spec,rest): three routes stop serving shapes their `responseSchema` never declared (#5882 #5950 #6442) + +Sweep #6487. One admission criterion: a route serves a response shape its +declared `responseSchema` does not describe. Three members, one direction each, +stated per member rather than picked for cheapness. + +**`GET /meta/:type/:name` — the ADR-0010 protection envelope is now declared +(#5950).** The uncached branch has always sent `lock` plus nine siblings on top +of `{ type, name, item }`, and `GetMetaItemResponseSchema` declared only the +three, so `.parse()` silently stripped every one of them. `lock` is the READ +half of the ADR-0008 optimistic-concurrency chain whose write half `#5745` +already declared — leaving it undeclared meant an SDK caller had to cast to read +it, the consumer-side tolerance Prime Directive #12 rejects. All ten keys are +declared **optional**, measured rather than assumed: the cached branch (the +default, `enableCache: true`) rebuilds the envelope as three keys and resolves +no lock at all, so `optional` here means "this branch did not publish it", never +"unlocked". Zero runtime change. Whether lock presence should depend on a cache +setting at all is the larger question #5950 raises and is deliberately left open. + +**`?layers=true` becomes `GET /meta/:type/:name/layers` (#5882).** The flag made +one route answer two unrelated resource representations — the ordinary envelope, +and a three-layer diagnostic projection (`code` / `overlay` / `effective`) that +drives Studio's "code default vs override vs effective" tabs — while the route +declared a single `responseSchema`. Anything generating a client from the route +table wrote a parser that was simply wrong for the flagged call. Per the +maintainer's ruling the projection gets its own path and its own +`GetMetaItemLayeredResponseSchema`: one path, one shape. The alternative — +teaching the route declaration to express "two shapes chosen by a query flag" — +was rejected as a new primitive every future tool would have to understand, and +conditional response selection is exactly where codegen and AI-written clients +go wrong. + +The `?layers=` spelling still answers the identical body during a deprecation +window (both entry points run one helper, so the two cannot drift), and now +carries `Deprecation: true` plus a `Link` header naming its successor. No +`Sunset` date: choosing the hard cut-off is a maintainer call. + +**`GET /analytics/meta` narrows to what it serves (#6442).** +`AnalyticsMetadataResponseSchema.data` declared `{ cubes: CubeSchema[] }` while +both implementations of `AnalyticsService.getMeta` return a bare `CubeMeta[]` +that the runtime hands to `success()` verbatim. A client written against the +published contract read `data.cubes` and got `undefined`; validating a live +response against the schema failed outright. Per the maintainer's ruling the +declaration narrows to the `CubeMeta[]` projection — zero runtime change — and +the generated `references/api/analytics.mdx`, which was publishing the wrong +shape, corrects itself. If a dashboard ever needs `format` or `description`, the +recorded return path is to add the key to the `CubeMeta` projection (additive); +widening the endpoint back to full cube definitions would push each cube's `sql` +to clients and is not revisited. diff --git a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md index fb2fa60a11..84333fbaa9 100644 --- a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md +++ b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md @@ -264,7 +264,7 @@ directory rather than per file. | Dir | Sites | |---|---| | `ai/` | 77 | -| `api/` | 391 | +| `api/` | 393 | | `cloud/` | 83 | | `identity/` | 33 | | `integration/` | 10 | diff --git a/packages/rest/src/meta-item-layered-route.test.ts b/packages/rest/src/meta-item-layered-route.test.ts new file mode 100644 index 0000000000..2e92f43f8b --- /dev/null +++ b/packages/rest/src/meta-item-layered-route.test.ts @@ -0,0 +1,236 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * `GET /meta/:type/:name/layers` — the three-layer diagnostic projection as its + * own resource, and the deprecation window on the `?layers=true` spelling it + * replaces (#5882). + * + * ## What was wrong + * + * One route answered TWO unrelated resource representations, chosen by a query + * flag: the ordinary read's `{ type, name, item, … }` envelope, and — under + * `?layers=true` — a projection with the packaged baseline, the tenant overlay + * and the merged result side by side. `packages/spec` declared exactly one + * `responseSchema` for the route, so anything generating a client from the + * route table produced a parser that was simply wrong for the flagged call. + * + * The maintainer ruled (2026-08-06) for one path per response shape rather than + * teaching the route declaration to express "two shapes, chosen by a flag": + * conditional response selection is a new primitive every future tool would + * have to understand, and it is precisely where codegen and AI-written clients + * go wrong. + * + * ## What these tests hold down + * + * 1. the new path answers the layered projection, with the three layers SEPARATE + * (collapsing them would delete the diagnostic, which is the whole reason the + * shape exists); + * 2. the deprecated `?layers=` spelling still answers the IDENTICAL body during + * its window — a deprecation that silently changed the body would be a + * breakage wearing a deprecation label — and advertises its successor; + * 3. the ordinary read is undisturbed by either. + */ + +import { describe, it, expect, vi } from 'vitest'; +import { RestServer } from './rest-server'; +import { GetMetaItemLayeredResponseSchema } from '@objectstack/spec/api'; + +const ANON_API = { api: { requireAuth: false } }; + +const CUSTOMER = { name: 'customer', label: 'Customer', fields: { id: { type: 'text' } } }; + +/** The real `getMetaItemLayered` return shape, as `metadata-protocol` builds it. */ +const LAYERED = { + type: 'object', + name: 'customer', + code: { name: 'customer', label: 'Customer' }, + overlay: { label: 'Client' }, + overlayScope: 'org', + effective: { name: 'customer', label: 'Client' }, + _diagnostics: { valid: true }, + lock: 'none', + editable: true, + deletable: true, + resettable: false, +}; + +function mockServer() { + return { + get: vi.fn(), post: vi.fn(), put: vi.fn(), delete: vi.fn(), patch: vi.fn(), + use: vi.fn(), listen: vi.fn().mockResolvedValue(undefined), close: vi.fn().mockResolvedValue(undefined), + }; +} + +function mockRes() { + const headers: Record = {}; + return { + json: vi.fn(), + status: vi.fn().mockReturnThis(), + header: vi.fn((k: string, v: string) => { headers[k] = v; }), + send: vi.fn(), + headers, + }; +} + +function baseProtocol(overrides: Record = {}) { + return { + getDiscovery: vi.fn().mockResolvedValue({ + version: 'v0', + routes: { data: '', metadata: '', ui: '', auth: '/auth' }, + }), + getMetaTypes: vi.fn().mockResolvedValue([]), + getMetaItems: vi.fn().mockResolvedValue([]), + getMetaItem: vi.fn(async ({ type, name }: any) => ({ + type, name, item: CUSTOMER, lock: 'none', editable: true, deletable: true, resettable: false, + })), + getMetaItemLayered: vi.fn(async ({ type, name }: any) => ({ ...LAYERED, type, name })), + getMetaItemCached: undefined as any, + findData: vi.fn().mockResolvedValue([]), + getData: vi.fn().mockResolvedValue({}), + createData: vi.fn().mockResolvedValue({ id: '1' }), + updateData: vi.fn().mockResolvedValue({}), + deleteData: vi.fn().mockResolvedValue({ success: true }), + ...overrides, + }; +} + +function routeFor(rest: RestServer, path: string) { + return (rest as any).getRoutes().find((r: any) => r.method === 'GET' && r.path === path); +} + +async function dispatch(protocol: any, path: string, params: any, query: any = {}) { + const rest = new RestServer(mockServer() as any, protocol as any, ANON_API as any); + (rest as any).resolveExecCtx = async () => ({ userId: 'u1', systemPermissions: [] }); + rest.registerRoutes(); + const route = routeFor(rest, path); + if (!route) throw new Error(`route not registered: GET ${path}`); + const res = mockRes(); + await route.handler({ params, query, headers: {} }, res); + return { res, body: res.json.mock.calls.at(-1)?.[0] }; +} + +const LAYERS_PATH = '/api/v1/meta/:type/:name/layers'; +const ITEM_PATH = '/api/v1/meta/:type/:name'; + +describe('#5882 GET /meta/:type/:name/layers — the declared layered resource', () => { + it('is registered as its own route', async () => { + const rest = new RestServer(mockServer() as any, baseProtocol() as any, ANON_API as any); + rest.registerRoutes(); + expect(routeFor(rest, LAYERS_PATH)).toBeDefined(); + }); + + it('is registered BEFORE the routes that would otherwise capture its path', async () => { + // `/:type/:name` cannot match a 3-segment path, but + // `/:type/:section/:name` CAN — it would bind section=, + // name="layers" and answer an ordinary metadata read for an item called + // "layers". Under a first-match router, registration order is the only + // thing preventing that, so the order is the assertion. + const rest = new RestServer(mockServer() as any, baseProtocol() as any, ANON_API as any); + rest.registerRoutes(); + const paths = (rest as any).getRoutes() + .filter((r: any) => r.method === 'GET') + .map((r: any) => r.path); + const layers = paths.indexOf(LAYERS_PATH); + const compound = paths.indexOf('/api/v1/meta/:type/:section/:name'); + expect(layers).toBeGreaterThanOrEqual(0); + expect(compound).toBeGreaterThanOrEqual(0); + expect(layers).toBeLessThan(compound); + }); + + it('answers the three-layer projection, with the layers SEPARATE', async () => { + const protocol = baseProtocol(); + const { body } = await dispatch(protocol, LAYERS_PATH, { type: 'object', name: 'customer' }); + + expect(protocol.getMetaItemLayered).toHaveBeenCalled(); + // The diagnostic's substance: `code` is the packaged baseline, `overlay` + // the customization row ALONE, `effective` the merge — three distinct + // values, not one merged document. + expect(body.code).toEqual({ name: 'customer', label: 'Customer' }); + expect(body.overlay).toEqual({ label: 'Client' }); + expect(body.effective).toEqual({ name: 'customer', label: 'Client' }); + expect(body.overlayScope).toBe('org'); + expect(body.code).not.toEqual(body.effective); + // And it is NOT the ordinary envelope. + expect(body.item).toBeUndefined(); + }); + + it('answers a body that parses against its DECLARED schema', async () => { + // The point of the member: the shape on the wire and the shape + // `packages/spec` declares for this path are now the same object. + const { body } = await dispatch(baseProtocol(), LAYERS_PATH, { type: 'object', name: 'customer' }); + const parsed = GetMetaItemLayeredResponseSchema.safeParse(body); + expect(parsed.error?.issues ?? []).toEqual([]); + expect(parsed.success).toBe(true); + }); + + it('threads `?package=` through for the package-scoped editor view (ADR-0048)', async () => { + const protocol = baseProtocol(); + await dispatch(protocol, LAYERS_PATH, { type: 'object', name: 'customer' }, { package: 'com.acme.crm' }); + expect(protocol.getMetaItemLayered).toHaveBeenCalledWith( + expect.objectContaining({ packageId: 'com.acme.crm' }), + ); + }); + + it('answers 501 when the protocol implements no layered view', async () => { + // A dedicated path must not fall through to the plain read the way the + // `?layers=` flag did — that would answer a different resource under a + // shape this path never declares. + const protocol = baseProtocol({ getMetaItemLayered: undefined }); + const { res, body } = await dispatch(protocol, LAYERS_PATH, { type: 'object', name: 'customer' }); + expect(res.status).toHaveBeenCalledWith(501); + expect(body.code).toBe('NOT_IMPLEMENTED'); + }); +}); + +describe('#5882 `?layers=true` — the deprecation window', () => { + it('still answers the layered body, byte for byte the same as the new path', async () => { + // The window's whole promise. Both entry points run ONE helper, and this + // is what would fail if someone re-forked them. + const viaFlag = await dispatch( + baseProtocol(), ITEM_PATH, { type: 'object', name: 'customer' }, { layers: 'true' }, + ); + const viaPath = await dispatch( + baseProtocol(), LAYERS_PATH, { type: 'object', name: 'customer' }, + ); + expect(viaFlag.body).toEqual(viaPath.body); + }); + + it('advertises the successor path in machine-readable headers', async () => { + const { res } = await dispatch( + baseProtocol(), ITEM_PATH, { type: 'object', name: 'customer' }, { layers: 'true' }, + ); + expect(res.header).toHaveBeenCalledWith('Deprecation', 'true'); + expect(res.headers.Link).toBe( + '; rel="successor-version"', + ); + }); + + it('does not mark the ordinary read deprecated', async () => { + const { res } = await dispatch(baseProtocol(), ITEM_PATH, { type: 'object', name: 'customer' }); + expect(res.header).not.toHaveBeenCalledWith('Deprecation', 'true'); + }); +}); + +describe('#5882 the ordinary read is undisturbed', () => { + it('answers the `{ type, name, item }` envelope with its protection keys', async () => { + const protocol = baseProtocol(); + const { body } = await dispatch(protocol, ITEM_PATH, { type: 'object', name: 'customer' }); + + expect(protocol.getMetaItemLayered).not.toHaveBeenCalled(); + expect(body).toMatchObject({ type: 'object', name: 'customer', lock: 'none', editable: true }); + expect(body.item).toMatchObject({ name: 'customer', label: 'Customer' }); + // No layer leaked into the ordinary envelope. + expect(body.code).toBeUndefined(); + expect(body.overlay).toBeUndefined(); + expect(body.effective).toBeUndefined(); + }); + + it('treats `?layers=` with an empty value as NOT a layered request', async () => { + // Pre-existing semantics, pinned so the new route does not quietly + // change which requests are layered. + const protocol = baseProtocol(); + const { body } = await dispatch(protocol, ITEM_PATH, { type: 'object', name: 'customer' }, { layers: '' }); + expect(protocol.getMetaItemLayered).not.toHaveBeenCalled(); + expect(body.item).toMatchObject({ label: 'Customer' }); + }); +}); diff --git a/packages/rest/src/rest-server.ts b/packages/rest/src/rest-server.ts index 165b323622..d95cbbbe98 100644 --- a/packages/rest/src/rest-server.ts +++ b/packages/rest/src/rest-server.ts @@ -2806,6 +2806,41 @@ export class RestServer { }; } + /** + * Serve the three-layer diagnostic projection (`code` / `overlay` / + * `effective`) declared by `GetMetaItemLayeredResponseSchema`. + * + * ONE implementation behind TWO entry points (#5882): the canonical + * `GET /meta/:type/:name/layers`, and the deprecated + * `GET /meta/:type/:name?layers=true` it replaces. Extracted rather than + * duplicated precisely because the deprecation window's promise is that the + * old spelling answers *the same body* — two copies would let that stop + * being true without anything failing. + * + * Not translated and not cached, both deliberately: this is a diagnostic + * view of what is STORED at each layer, so locale-collapsing it (or serving + * it from the published-value cache) would misreport the thing being + * diagnosed. + */ + private async serveMetaItemLayered( + req: any, + res: any, + environmentId: string | undefined, + p: any, + ): Promise { + // ADR-0048 — thread `?package=` so the layered (Studio editor) view is + // package-scoped; the editor passes the edited item's owning package, + // not the studio app's. + const layeredPackageId = req.query?.package || undefined; + const layered = await p.getMetaItemLayered({ + type: req.params.type, + name: req.params.name, + ...(layeredPackageId ? { packageId: layeredPackageId } : {}), + ...(environmentId ? { environmentId } : {}), + }); + res.json(layered); + } + /** * Translate a list of metadata documents using `translateMetaItem`. */ @@ -4126,6 +4161,52 @@ export class RestServer { }, }); + // [#5882] GET /meta/:type/:name/layers — the three-layer diagnostic + // projection as its OWN resource. Registered BEFORE + // /meta/:type/:name for the same first-match reason as + // /references above, and before /meta/:type/:section/:name, which + // would otherwise capture this path with section=, + // name="layers". + // + // This path exists because the projection used to be reachable only + // as `GET /meta/:type/:name?layers=true` — the same route answering + // a SECOND, undeclared body shape depending on a query flag, while + // `packages/spec` declared one `responseSchema` for it. The ruled + // fix (maintainer, 2026-08-06) was one path per response shape, + // deliberately NOT teaching the route declaration to express + // "two shapes chosen by a flag": that would add a primitive every + // future tool has to understand, and conditional response selection + // is exactly where codegen and AI-written clients go wrong. + this.routeManager.register({ + method: 'GET', + path: `${metaPath}/:type/:name/layers`, + handler: async (req: any, res: any) => { + try { + const environmentId = isScoped ? req.params?.environmentId : undefined; + const p = await this.resolveProtocol(environmentId, req); + if (typeof (p as any).getMetaItemLayered !== 'function') { + // A dedicated path cannot fall through to the plain + // read the way the `?layers=` flag did — answering + // the merged `{ type, name, item }` envelope here + // would be answering a different resource with a + // shape this path never declares. + res.status(501).json({ + error: 'Layered metadata view not supported by protocol implementation', + code: 'NOT_IMPLEMENTED', + }); + return; + } + await this.serveMetaItemLayered(req, res, environmentId, p); + } catch (error: any) { + handleRouteError(res, error); + } + }, + metadata: { + summary: 'Get a metadata item as its three layers (code / overlay / effective)', + tags: ['metadata'], + }, + }); + // ADR-0046 §6 — GET /meta/book/:name/tree // Resolve a book spine against the docs that exist *now* into a // rendered tree (membership is DERIVED, never stored — §6.2.1). An @@ -4264,32 +4345,42 @@ export class RestServer { // Skips the cache path entirely — layered view is a // diagnostic endpoint, not on the hot read path. // - // [#5563] This is a DIFFERENT RESOURCE reached through a - // query flag, not a variant body of the same one: it - // answers `{ type, name, code, overlay, overlayScope, - // effective, validation }` — three layers side by side, - // where `effective` is what the plain read would return. - // Collapsing it into `GetMetaItemResponseSchema`'s single - // `item` would delete the diagnostic (the whole point is - // seeing code vs overlay vs effective separately), so the - // convergence deliberately stops at the ordinary read. - // What remains is a spec gap, not a runtime split: the - // route declares ONE `responseSchema` while `?layers=` - // answers a second, undeclared shape — filed as #5882 - // for a spec seat. + // [#5563 → #5882] DEPRECATED SPELLING. This flag makes one + // route answer a SECOND resource representation — three + // layers side by side (`code` / `overlay` / `effective`), + // where `effective` is what the plain read returns — while + // the route declares a single `responseSchema`. #5563 + // converged the ordinary read and left this half open + // because collapsing the layers into + // `GetMetaItemResponseSchema`'s single `item` would delete + // the diagnostic outright. + // + // #5882 closed it the other way (maintainer ruling, 2026-08-06): + // the projection is now its own path, + // `GET /meta/:type/:name/layers`, declared by + // `GetMetaItemLayeredResponseSchema`. One path, one shape. + // + // This branch stays for a deprecation window so existing + // callers (Studio's metadata editor) are not broken by the + // move. It answers the IDENTICAL body — same helper, not a + // copy — and advertises the successor in the response + // headers, so a client can discover the migration without + // reading the changelog. Delete this branch (and the + // headers with it) once the callers have moved. const wantLayered = req.query?.layers !== undefined && req.query?.layers !== ''; if (wantLayered && typeof (p as any).getMetaItemLayered === 'function') { - // ADR-0048 — thread `?package=` so the layered (Studio - // editor) view is package-scoped; the editor passes the - // edited item's owning package, not the studio app's. - const layeredPackageId = req.query?.package || undefined; - const layered = await (p as any).getMetaItemLayered({ - type: req.params.type, - name: req.params.name, - ...(layeredPackageId ? { packageId: layeredPackageId } : {}), - ...(environmentId ? { environmentId } : {}), - }); - res.json(layered); + // RFC 9745 `Deprecation` + RFC 8288 `Link` — the same + // machine-readable pairing `versioning.zod.ts` already + // describes for retiring API versions, applied to a + // retiring query flag. No `Sunset` date: choosing the + // hard cut-off is a maintainer call, and an invented + // date is worse than none. + res.header('Deprecation', 'true'); + res.header( + 'Link', + `<${metaPath}/${req.params.type}/${req.params.name}/layers>; rel="successor-version"`, + ); + await this.serveMetaItemLayered(req, res, environmentId, p); return; } diff --git a/packages/spec/authorable-surface/api.json b/packages/spec/authorable-surface/api.json index 0d5c357a9e..6d7981d3c4 100644 --- a/packages/spec/authorable-surface/api.json +++ b/packages/spec/authorable-surface/api.json @@ -411,6 +411,13 @@ "api/CrudEndpointsConfig:objectParamStyle", "api/CrudEndpointsConfig:operations", "api/CrudEndpointsConfig:patterns", + "api/CubeMeta:dimensions", + "api/CubeMeta:measures", + "api/CubeMeta:name", + "api/CubeMeta:title", + "api/CubeMetaMember:name", + "api/CubeMetaMember:title", + "api/CubeMetaMember:type", "api/CursorMessage:cursor", "api/CursorMessage:messageId", "api/CursorMessage:timestamp", @@ -711,11 +718,38 @@ "api/GetMetaItemCachedResponse:lastModified", "api/GetMetaItemCachedResponse:notModified", "api/GetMetaItemCachedResponse:version", + "api/GetMetaItemLayeredResponse:_diagnostics", + "api/GetMetaItemLayeredResponse:code", + "api/GetMetaItemLayeredResponse:deletable", + "api/GetMetaItemLayeredResponse:editable", + "api/GetMetaItemLayeredResponse:effective", + "api/GetMetaItemLayeredResponse:lock", + "api/GetMetaItemLayeredResponse:lockDocsUrl", + "api/GetMetaItemLayeredResponse:lockReason", + "api/GetMetaItemLayeredResponse:lockSource", + "api/GetMetaItemLayeredResponse:name", + "api/GetMetaItemLayeredResponse:overlay", + "api/GetMetaItemLayeredResponse:overlayScope", + "api/GetMetaItemLayeredResponse:packageId", + "api/GetMetaItemLayeredResponse:packageVersion", + "api/GetMetaItemLayeredResponse:provenance", + "api/GetMetaItemLayeredResponse:resettable", + "api/GetMetaItemLayeredResponse:type", "api/GetMetaItemRequest:name", "api/GetMetaItemRequest:packageId", "api/GetMetaItemRequest:type", + "api/GetMetaItemResponse:deletable", + "api/GetMetaItemResponse:editable", "api/GetMetaItemResponse:item", + "api/GetMetaItemResponse:lock", + "api/GetMetaItemResponse:lockDocsUrl", + "api/GetMetaItemResponse:lockReason", + "api/GetMetaItemResponse:lockSource", "api/GetMetaItemResponse:name", + "api/GetMetaItemResponse:packageId", + "api/GetMetaItemResponse:packageVersion", + "api/GetMetaItemResponse:provenance", + "api/GetMetaItemResponse:resettable", "api/GetMetaItemResponse:type", "api/GetMetaItemsRequest:packageId", "api/GetMetaItemsRequest:type", diff --git a/packages/spec/json-schema.manifest/api.json b/packages/spec/json-schema.manifest/api.json index 0dda14515f..08ea46d28f 100644 --- a/packages/spec/json-schema.manifest/api.json +++ b/packages/spec/json-schema.manifest/api.json @@ -93,6 +93,8 @@ "api/CrudEndpointPattern", "api/CrudEndpointsConfig", "api/CrudOperation", + "api/CubeMeta", + "api/CubeMetaMember", "api/CursorMessage", "api/CursorPosition", "api/DataEvent", @@ -174,6 +176,7 @@ "api/GetLocalesResponse", "api/GetMetaItemCachedRequest", "api/GetMetaItemCachedResponse", + "api/GetMetaItemLayeredResponse", "api/GetMetaItemRequest", "api/GetMetaItemResponse", "api/GetMetaItemsRequest", diff --git a/packages/spec/src/api/analytics.test.ts b/packages/spec/src/api/analytics.test.ts index e7660206fe..49e4a8a9ab 100644 --- a/packages/spec/src/api/analytics.test.ts +++ b/packages/spec/src/api/analytics.test.ts @@ -7,6 +7,28 @@ import { AnalyticsMetadataResponseSchema, AnalyticsSqlResponseSchema, } from './analytics.zod'; +import type { AnalyticsMetadataResponse } from './analytics.zod'; +import type { CubeMeta } from '../contracts/analytics-service'; + +/** Type-level identity: true iff A and B are the same type. */ +type Eq< A, B > = (< T >() => T extends A ? 1 : 2) extends (< T >() => T extends B ? 1 : 2) ? true : false; +/** Compile error when the argument is not `true`. */ +type Assert< T extends true > = T; + +/** + * #6442 — the declared element of `AnalyticsMetadataResponse.data` IS the + * `CubeMeta` contract interface, not merely shaped like it. + * + * `CubeMeta` in `contracts/analytics-service.ts` is what both `getMeta` + * implementations return, and it was already correct while + * `AnalyticsMetadataResponseSchema` was not — `packages/spec` stated one shape + * in two files and let them disagree. Binding them here is what stops that + * happening a second time: narrow either one alone and this goes red. + * + * Exported deliberately — an unread alias inside a test body is TS6196, and a + * `@ts-expect-error`-style pin no program compiles is no pin at all. + */ +export type CubeMetaMatchesContract = Assert< Eq< AnalyticsMetadataResponse['data'][number], CubeMeta > >; describe('AnalyticsEndpoint', () => { it('should accept all valid endpoints', () => { @@ -156,55 +178,104 @@ describe('GetAnalyticsMetaRequestSchema', () => { }); }); -describe('AnalyticsMetadataResponseSchema', () => { - it('should accept valid metadata response', () => { - const resp = AnalyticsMetadataResponseSchema.parse({ - success: true, - data: { - cubes: [ - { - name: 'orders', - sql: 'SELECT * FROM orders', - measures: { - total_revenue: { - name: 'total_revenue', - label: 'Total Revenue', - type: 'sum', - sql: 'amount', - }, - }, - dimensions: { - status: { - name: 'status', - label: 'Status', - type: 'string', - sql: 'status', - }, - }, - }, +/** + * #6442 — `data` is the `CubeMeta[]` discovery projection the endpoint really + * serves, NOT the `{ cubes: CubeSchema[] }` wrapper it used to declare. + * + * The three cases here previously pinned the old declaration, and every one of + * them was re-judged rather than re-spelled: the first pinned a body no + * implementation has ever produced (replaced with the real one), the second only + * needed the wrapper dropped, and the third — "should reject missing cubes" — + * would have kept passing against the new schema for the WRONG reason (`{}` is + * not an array either), pinning a `cubes` key that no longer exists. It is + * replaced by the assertion that carries the actual load of this change: the + * previously-declared shape is now rejected. + */ +describe('AnalyticsMetadataResponseSchema — the CubeMeta[] projection (#6442)', () => { + /** + * A real `GET /analytics/meta` body: what `AnalyticsService.getMeta` and its + * `driver-memory` twin both build — measure/dimension names CUBE-QUALIFIED, + * `title` projected from the definition's `label`, and no `sql` anywhere. + */ + const SERVED_BODY = { + success: true, + data: [ + { + name: 'orders', + title: 'Orders', + measures: [ + { name: 'orders.total_revenue', type: 'sum', title: 'Total Revenue' }, + ], + dimensions: [ + { name: 'orders.status', type: 'string', title: 'Status' }, ], }, - }); - expect(resp.data.cubes).toHaveLength(1); - expect(resp.data.cubes[0].name).toBe('orders'); + ], + }; + + it('accepts the body the endpoint actually serves', () => { + const resp = AnalyticsMetadataResponseSchema.parse(SERVED_BODY); + expect(resp.data).toHaveLength(1); + expect(resp.data[0].name).toBe('orders'); + // The substance of the projection: qualified member names, display titles, + // and no `sql` reaching the client. + expect(resp.data[0].measures[0].name).toBe('orders.total_revenue'); + expect(resp.data[0].measures[0].title).toBe('Total Revenue'); + expect(resp.data[0].dimensions[0].name).toBe('orders.status'); + expect(resp.data[0]).not.toHaveProperty('sql'); }); - it('should accept empty cubes list', () => { - const resp = AnalyticsMetadataResponseSchema.parse({ - success: true, - data: { cubes: [] }, - }); - expect(resp.data.cubes).toHaveLength(0); + it('accepts an empty cube list', () => { + const resp = AnalyticsMetadataResponseSchema.parse({ success: true, data: [] }); + expect(resp.data).toHaveLength(0); }); - it('should reject missing cubes', () => { + it('REJECTS the previously-declared `{ cubes: CubeSchema[] }` wrapper', () => { + // This is the direction of the fix, stated as a test: the old declaration + // described a body no implementation produced, so a client written against + // it read `data.cubes` and got `undefined`. That shape must now fail loudly + // rather than be quietly accepted alongside the real one. expect(() => AnalyticsMetadataResponseSchema.parse({ success: true, - data: {}, - }) + data: { + cubes: [ + { + name: 'orders', + sql: 'SELECT * FROM orders', + measures: { total_revenue: { name: 'total_revenue', label: 'Total Revenue', type: 'sum', sql: 'amount' } }, + dimensions: { status: { name: 'status', label: 'Status', type: 'string', sql: 'status' } }, + }, + ], + }, + }), + ).toThrow(); + }); + + it('requires each measure/dimension to carry `name` and `type`', () => { + expect(() => + AnalyticsMetadataResponseSchema.parse({ + success: true, + data: [{ name: 'orders', measures: [{ title: 'Total Revenue' }], dimensions: [] }], + }), ).toThrow(); }); + + it('declares exactly the `CubeMeta` contract shape, element for element', () => { + // The compile-time half is `CubeMetaMatchesContract` at module scope below — + // it must be EXPORTED to be a real check: a type alias declared inside this + // function body and never read is TS6196 under `noUnusedLocals`, and + // deleting it would leave every gate just as green (the phantom-check shape + // AGENTS.md names). + // + // Runtime half: a value typed as the contract parses against the schema. + const fromContract: CubeMeta = { + name: 'orders', + measures: [{ name: 'orders.total_revenue', type: 'sum' }], + dimensions: [{ name: 'orders.status', type: 'string' }], + }; + expect(() => AnalyticsMetadataResponseSchema.parse({ success: true, data: [fromContract] })).not.toThrow(); + }); }); describe('AnalyticsSqlResponseSchema', () => { diff --git a/packages/spec/src/api/analytics.zod.ts b/packages/spec/src/api/analytics.zod.ts index f130298bbb..bfec7e04b5 100644 --- a/packages/spec/src/api/analytics.zod.ts +++ b/packages/spec/src/api/analytics.zod.ts @@ -1,7 +1,7 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { z } from 'zod'; -import { AnalyticsQuerySchema, CubeSchema } from '../data/analytics.zod'; +import { AnalyticsQuerySchema } from '../data/analytics.zod'; import { BaseResponseSchema } from './contract.zod'; import { retiredKey } from '../shared/retired-key'; @@ -87,14 +87,87 @@ export const GetAnalyticsMetaRequestSchema = lazySchema(() => z.object({ cube: z.string().optional().describe('Optional cube name to filter'), })); +/** + * A measure or dimension as `GET /analytics/meta` publishes it — the + * discovery projection, not the authoring definition. + * + * `name` is CUBE-QUALIFIED (`"."`), which is the form + * `/analytics/query` expects back in `measures[]` / `dimensions[]`; the + * unqualified key it was defined under is not published. `title` carries the + * definition's `label`, so it is the display name a dashboard renders. + * + * Deliberately narrower than the authoring definitions (`MetricSchema` / + * `DimensionSchema` in `data/analytics.zod.ts`): `sql`, `filters`, + * `description`, `granularities` and `format` are dropped by the projection and + * are NOT reachable through this endpoint (#6442). + * + * No bare type alias by design — `CubeMeta` in `contracts/analytics-service.ts` + * is already THE name for this shape, and a second name for one type is the + * permanent synonym ADR-0122 D3 forbids. `analytics-meta-response.test.ts` pins + * the two against each other at compile time. + */ +export const CubeMetaMemberSchema = lazySchema(() => z.object({ + name: z.string().describe('Cube-qualified member name, `"."` — the spelling `/analytics/query` accepts'), + type: z.string().describe( + 'Aggregation type for a measure (`AggregationMetricType`) or data type for a ' + + 'dimension (`DimensionType`). Declared as a string rather than either enum ' + + 'because the projection copies the value through verbatim and this one ' + + 'shape serves both member kinds.', + ), + title: z.string().optional().describe('Display label, projected from the definition\'s `label`'), +})); + +/** + * One cube as `GET /analytics/meta` publishes it — the `CubeMeta` discovery + * projection declared in `contracts/analytics-service.ts` and produced + * identically by both implementations of `AnalyticsService.getMeta`. + * + * Carries only what a client needs to BUILD a query (which cubes exist, and + * which measures/dimensions each accepts). The cube's `sql`, `joins`, + * `refreshKey`, `public` and `description` are not projected — `CubeSchema` in + * `data/analytics.zod.ts` remains the authoring definition. + * + * No bare type alias — see the note on {@link CubeMetaMemberSchema}. + */ +export const CubeMetaSchema = lazySchema(() => z.object({ + name: z.string().describe('Cube name'), + title: z.string().optional().describe('Human-readable cube title'), + measures: z.array(CubeMetaMemberSchema).describe('Measures this cube accepts in `/analytics/query`'), + dimensions: z.array(CubeMetaMemberSchema).describe('Dimensions this cube accepts in `/analytics/query`'), +})); + /** * Meta Response - * Returns available cubes, metrics, and dimensions. + * + * Describes the body `GET /api/v1/analytics/meta` actually returns: `data` is a + * BARE ARRAY of the `CubeMeta` discovery projection — not `{ cubes: Cube[] }` + * (#6442, ruled by the maintainer 2026-08-08 as "narrow the declaration"). + * + * The previous declaration described a shape the endpoint has never served, in + * either implementation: `AnalyticsService.getMeta` + * (`service-analytics/src/analytics-service.ts`) and its `driver-memory` twin + * (`memory-analytics.ts`) both answer `Promise< CubeMeta[] >`, and + * `runtime/src/domains/analytics.ts` hands that array to `success()` verbatim, + * so it lands directly under `data`. A client written against the old + * declaration read `data.cubes` and got `undefined`; a client that validated a + * live response against this schema failed outright. `packages/spec` stated + * both shapes itself — the TS contract + * (`contracts/analytics-service.ts`, `getMeta(): Promise< CubeMeta[] >`) already + * agreed with the runtime, and this schema was the lone outlier. + * + * Zero runtime change: the wire body is untouched, only its declaration moves. + * + * **Return path if more keys are ever needed** (recorded with the ruling): add + * the key to the projection above — additive and backwards compatible. ⛔ Do NOT + * widen this endpoint back to full `CubeSchema` definitions: that would publish + * each cube's `sql` to every client, a capability expansion with no measured + * consumer pulling it. */ export const AnalyticsMetadataResponseSchema = lazySchema(() => BaseResponseSchema.extend({ - data: z.object({ - cubes: z.array(CubeSchema).describe('Available cubes'), - }), + data: z.array(CubeMetaSchema).describe( + 'Available cubes, each as the `CubeMeta` discovery projection. A bare array — ' + + 'there is no `cubes` wrapper object.', + ), })); // ========================================== diff --git a/packages/spec/src/api/meta-item-response-shapes.test.ts b/packages/spec/src/api/meta-item-response-shapes.test.ts new file mode 100644 index 0000000000..ae53a6cf69 --- /dev/null +++ b/packages/spec/src/api/meta-item-response-shapes.test.ts @@ -0,0 +1,214 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * The two response shapes `GET /api/v1/meta/:type/:name…` serves, each declared + * by its own schema (sweep #6487). + * + * Both members here are DECLARATION changes with zero runtime effect, so the + * assertions have to bear the load themselves: it is not enough that the new + * keys exist in the file, they must be reachable at the type level (the thing a + * consumer was previously forced to `cast` for) and the real wire bodies must + * parse against them. + * + * - **#5950** — the uncached branch of `GET /meta/:type/:name` has always sent + * the ADR-0010 protection envelope (`lock` and nine siblings) on top of + * `{ type, name, item }`, and `GetMetaItemResponseSchema` declared only the + * three. `lock` is the READ half of the ADR-0008 optimistic-concurrency chain + * whose write half `SaveMetaItemResponseSchema` already declares (#5745). + * - **#5882** — `?layers=true` answered a completely different projection that + * the route never declared. It now has its own path and its own schema. + */ + +import { describe, it, expect } from 'vitest'; +import { + GetMetaItemResponseSchema, + GetMetaItemLayeredResponseSchema, +} from './protocol.zod'; +import type { GetMetaItemResponse, GetMetaItemLayeredResponse } from './protocol.zod'; +import type { MetadataLock } from '../kernel/metadata-protection.zod'; + +/** Type-level identity / assignability helpers. */ +type Eq< A, B > = (< T >() => T extends A ? 1 : 2) extends (< T >() => T extends B ? 1 : 2) ? true : false; +type Assert< T extends true > = T; + +/** + * The compile-time half of both members, at module scope and EXPORTED. + * + * These have to live here rather than inside an `it()` body for two reasons + * that both bite: an unread alias in a function body is TS6196 under + * `noUnusedLocals`, and — the one that matters — a pin no program compiles is a + * phantom check that stays green after someone deletes the thing it guards. + * `packages/spec` compiles its tests via `tsconfig.test.json`, so these are + * real: reverting either member turns them red at `pnpm typecheck`. + */ + +/** + * #5950 — `lock` is reachable AND typed as the ADR-0010 union. This is the + * #5545 complaint stated as a compile-time fact: an SDK caller holding a + * `GetMetaItemResponse` can branch on the lock's VALUES without a cast, which + * is what "the read side has a contract" has to mean. + */ +export type LockIsTheAdr0010Union = Assert< Eq< GetMetaItemResponse['lock'], MetadataLock | undefined > >; + +/** + * #5882 — the two responses are genuinely different resources: neither schema + * is a widening of the other, which is the fact that justifies a second path + * instead of one stretched declaration. + */ +export type LayeredCarriesNoItem = Assert< Eq< 'item' extends keyof GetMetaItemLayeredResponse ? true : false, false > >; +export type PlainCarriesNoLayers = Assert< Eq< 'effective' extends keyof GetMetaItemResponse ? true : false, false > >; + +/** The document under `item` — irrelevant to these shapes, so kept trivial. */ +const CUSTOMER = { name: 'customer', label: 'Customer' }; + +describe('#5950 GetMetaItemResponseSchema — the ADR-0010 protection envelope is declared', () => { + /** + * The uncached branch's real body: `metadata-protocol`'s `getMetaItem` + * return, spread onto the wire verbatim by `translateMetaEnvelope`. + * All ten protection keys, including the six conditional ones. + */ + const UNCACHED_BODY = { + type: 'object', + name: 'customer', + item: CUSTOMER, + lock: 'no-overlay', + lockReason: 'Shipped by the setup package', + lockSource: 'package', + lockDocsUrl: 'https://docs.example.test/locks', + provenance: 'package', + packageId: 'com.objectstack.setup', + packageVersion: '1.2.3', + editable: false, + deletable: true, + resettable: true, + }; + + it('parses the uncached body WITHOUT stripping the protection envelope', () => { + const parsed = GetMetaItemResponseSchema.parse(UNCACHED_BODY); + // The substance: every carrier survives the parse. Before this change + // `.parse()` silently dropped all ten — the same way the write side + // dropped `version` before #5745. + expect(parsed.lock).toBe('no-overlay'); + expect(parsed.lockReason).toBe('Shipped by the setup package'); + expect(parsed.lockSource).toBe('package'); + expect(parsed.lockDocsUrl).toBe('https://docs.example.test/locks'); + expect(parsed.provenance).toBe('package'); + expect(parsed.packageId).toBe('com.objectstack.setup'); + expect(parsed.packageVersion).toBe('1.2.3'); + expect(parsed.editable).toBe(false); + expect(parsed.deletable).toBe(true); + expect(parsed.resettable).toBe(true); + }); + + it('still parses the CACHED body, where every protection key is absent', () => { + // The default deployment (`enableCache: true`) rebuilds the envelope as + // exactly these three keys and resolves no lock. Declaring the envelope + // REQUIRED would have made the default path fail its own contract. + const parsed = GetMetaItemResponseSchema.parse({ type: 'object', name: 'customer', item: CUSTOMER }); + expect(parsed.type).toBe('object'); + expect(parsed.item).toEqual(CUSTOMER); + expect(parsed.lock).toBeUndefined(); + expect(parsed.editable).toBeUndefined(); + }); + + it('rejects a lock value outside the ADR-0010 vocabulary', () => { + // The declaration is not merely wider — it is the lock vocabulary. A value + // the resolver could never produce must not validate. + expect(() => + GetMetaItemResponseSchema.parse({ ...UNCACHED_BODY, lock: 'read-only' }), + ).toThrow(); + expect(() => + GetMetaItemResponseSchema.parse({ ...UNCACHED_BODY, lockSource: 'overlay' }), + ).toThrow(); + }); + + it('types `lock` as the ADR-0010 union — not `unknown`, so no cast is needed', () => { + // Compile-time half: `LockIsTheAdr0010Union` at module scope. Runtime half: + // a response literal that declares `lock` and narrows on it, with no cast. + const response: GetMetaItemResponse = { type: 'object', name: 'customer', item: CUSTOMER, lock: 'full' }; + // Reading it needs no cast; narrowing works. + const readOnly = response.lock === 'full' || response.lock === 'no-overlay'; + expect(readOnly).toBe(true); + }); +}); + +describe('#5882 GetMetaItemLayeredResponseSchema — the three-layer projection', () => { + /** What `getMetaItemLayered` returns, and `GET /meta/:type/:name/layers` serves. */ + const LAYERED_BODY = { + type: 'object', + name: 'customer', + code: { name: 'customer', label: 'Customer' }, + overlay: { label: 'Client' }, + overlayScope: 'org', + effective: { name: 'customer', label: 'Client' }, + _diagnostics: { valid: true }, + lock: 'none', + editable: true, + deletable: true, + resettable: false, + }; + + it('parses the layered body with all three layers intact', () => { + const parsed = GetMetaItemLayeredResponseSchema.parse(LAYERED_BODY); + // The whole point of the projection: the layers stay SEPARATE. Collapsing + // them into one `item` is what the ordinary read does, and doing it here + // would delete the diagnostic. + expect(parsed.code).toEqual({ name: 'customer', label: 'Customer' }); + expect(parsed.overlay).toEqual({ label: 'Client' }); + expect(parsed.effective).toEqual({ name: 'customer', label: 'Client' }); + expect(parsed.overlayScope).toBe('org'); + expect(parsed._diagnostics).toEqual({ valid: true }); + }); + + it('accepts an uncustomized item — null overlay, null scope', () => { + const parsed = GetMetaItemLayeredResponseSchema.parse({ + ...LAYERED_BODY, + overlay: null, + overlayScope: null, + }); + expect(parsed.overlay).toBeNull(); + expect(parsed.overlayScope).toBeNull(); + }); + + it('accepts an absent `_diagnostics` — types with no registered Zod schema', () => { + const { _diagnostics: _omitted, ...withoutDiagnostics } = LAYERED_BODY; + const parsed = GetMetaItemLayeredResponseSchema.parse(withoutDiagnostics); + expect(parsed._diagnostics).toBeUndefined(); + }); + + it('REQUIRES the four resolved verdicts — this path always sets them', () => { + // Unlike the ordinary read there is one producer path here (the layered + // view skips the cache), so these are guaranteed and declared required. + // A body missing them is not a layered response. + for (const key of ['lock', 'editable', 'deletable', 'resettable'] as const) { + const { [key]: _dropped, ...missing } = LAYERED_BODY; + expect(() => GetMetaItemLayeredResponseSchema.parse(missing)).toThrow(); + } + }); + + it('rejects an overlayScope outside `org` | `env`', () => { + expect(() => + GetMetaItemLayeredResponseSchema.parse({ ...LAYERED_BODY, overlayScope: 'package' }), + ).toThrow(); + }); + + it('is a DIFFERENT shape from the ordinary read — which is why it got its own path', () => { + // The layered body carries no `item`, and the ordinary envelope carries no + // layers. One route answering both is what #5882 was filed about; the + // assertion states that the two really are distinct resources rather than + // one schema that could have absorbed the other. + // Compile-time half: `LayeredCarriesNoItem` / `PlainCarriesNoLayers` at + // module scope. + const layered = GetMetaItemLayeredResponseSchema.parse(LAYERED_BODY); + expect(layered).not.toHaveProperty('item'); + + // And the ordinary schema cannot stand in for it at all: the layered body + // has no `item`, which `GetMetaItemResponseSchema` requires, so it does not + // merely lose the layers on the way through — it fails outright. That is + // the sharpest available statement that these are two resources and not one + // schema stretched over both. + const throughPlain = GetMetaItemResponseSchema.safeParse(LAYERED_BODY); + expect(throughPlain.success).toBe(false); + expect(throughPlain.error?.issues.map((i) => i.path.join('.'))).toContain('item'); + }); +}); diff --git a/packages/spec/src/api/plugin-rest-api.test.ts b/packages/spec/src/api/plugin-rest-api.test.ts index 4648e982f7..17b1166ee3 100644 --- a/packages/spec/src/api/plugin-rest-api.test.ts +++ b/packages/spec/src/api/plugin-rest-api.test.ts @@ -471,7 +471,14 @@ describe('plugin-rest-api.zod', () => { expect(DEFAULT_METADATA_ROUTES.service).toBe('metadata'); expect(DEFAULT_METADATA_ROUTES.category).toBe('metadata'); expect(DEFAULT_METADATA_ROUTES.authRequired).toBe(true); - expect(DEFAULT_METADATA_ROUTES.endpoints).toHaveLength(4); + // 4 -> 5: `GET /:type/:name/layers` (#5882) — the three-layer diagnostic + // projection, previously reachable only as an undeclared `?layers=true` + // variant of `GET /:type/:name`, now its own path with its own + // `GetMetaItemLayeredResponseSchema`. + expect(DEFAULT_METADATA_ROUTES.endpoints).toHaveLength(5); + expect(DEFAULT_METADATA_ROUTES.endpoints?.map((e) => `${e.method} ${e.path}`)).toContain( + 'GET /:type/:name/layers', + ); expect(DEFAULT_METADATA_ROUTES.middleware).toBeDefined(); }); diff --git a/packages/spec/src/api/plugin-rest-api.zod.ts b/packages/spec/src/api/plugin-rest-api.zod.ts index 69b7d5afdd..1aef52b556 100644 --- a/packages/spec/src/api/plugin-rest-api.zod.ts +++ b/packages/spec/src/api/plugin-rest-api.zod.ts @@ -736,7 +736,7 @@ export const DEFAULT_METADATA_ROUTES: RestApiRouteRegistration = { prefix: '/api/v1/meta', service: 'metadata', category: 'metadata', - methods: ['getMetaTypes', 'getMetaItems', 'getMetaItem', 'saveMetaItem'], + methods: ['getMetaTypes', 'getMetaItems', 'getMetaItem', 'getMetaItemLayered', 'saveMetaItem'], authRequired: true, endpoints: [ { @@ -783,6 +783,30 @@ export const DEFAULT_METADATA_ROUTES: RestApiRouteRegistration = { cacheable: true, cacheTtl: 3600, }, + { + method: 'GET', + path: '/:type/:name/layers', + handler: 'getMetaItemLayered', + category: 'metadata', + public: false, + summary: 'Get a metadata item as its three layers (code / overlay / effective)', + description: + 'Diagnostic projection powering Studio\'s "code default vs override vs effective" ' + + 'comparison: the packaged baseline, the tenant customization row, and the merged ' + + 'result side by side. A DIFFERENT representation from `GET /:type/:name`, which ' + + 'answers only the merged value under `item` — hence its own path and its own ' + + 'response schema (#5882). Reached until now only as `GET /:type/:name?layers=true`, ' + + 'which still works during its deprecation window but is answered with ' + + '`Deprecation` / `Link` headers pointing here.', + tags: ['Metadata'], + // No `requestSchema`, for the same reason as `GET /:type/:name` above + // (#3899): every input is path/query-bound, so no request body exists to + // validate and declaring one would advertise a gate that cannot run. + responseSchema: 'GetMetaItemLayeredResponseSchema', + // Not cacheable: this is a diagnostic read that deliberately bypasses the + // metadata cache so it always reflects the live overlay row. + cacheable: false, + }, { method: 'PUT', path: '/:type/:name', diff --git a/packages/spec/src/api/protocol.zod.ts b/packages/spec/src/api/protocol.zod.ts index 805fbeafc3..c456b5dfac 100644 --- a/packages/spec/src/api/protocol.zod.ts +++ b/packages/spec/src/api/protocol.zod.ts @@ -23,6 +23,15 @@ import { RealtimePresenceSchema, TransportProtocol } from './realtime.zod'; import { ObjectPermissionSchema, EffectiveObjectPermissionSchema, FieldPermissionSchema } from '../security/permission.zod'; import { ActionDescriptorSchema } from '../automation/node-executor.zod'; import { TranslationDataSchema } from '../system/translation.zod'; +// #5950 / #5882 — the ADR-0010 read-side protection envelope both metadata-item +// responses publish. Same three vocabularies the resolver filters against, so a +// value this spec cannot name is a value the resolver would have dropped. +import { + MetadataLockSchema, + MetadataLockSourceSchema, + MetadataProvenanceSchema, +} from '../kernel/metadata-protection.zod'; +import { MetadataValidationResultSchema } from '../kernel/metadata-plugin.zod'; import { ListPackagesRequestSchema, ListPackagesResponseSchema, @@ -228,13 +237,187 @@ export const GetMetaItemRequestSchema = lazySchema(() => z.object({ packageId: z.string().optional().describe('Optional package ID to filter items by'), })); +/** + * ADR-0010 read-side protection envelope — the flags a metadata READ publishes + * alongside the document, all derived from one `resolveLockState()` call. + * + * These are the UN-prefixed, envelope-level counterparts of the `_lock` / + * `_provenance` fields `MetadataProtectionFields` splices into the document + * itself: the document stores `_lock`, and the read RESOLVES it into `lock` + * plus the three `editable` / `deletable` / `resettable` verdicts Studio + * renders affordances from (ADR-0010 §5), so no consumer re-implements the + * lock algebra. + * + * Shared by {@link GetMetaItemResponseSchema} and + * {@link GetMetaItemLayeredResponseSchema} — both are produced by the SAME + * `resolveLockState` call in `metadata-protocol`, so a mixin is what keeps the + * two declarations from drifting apart key by key. Module-local on purpose: it + * is a shape these two responses share, not a new public vocabulary. + * + * Every key is optional HERE and tightened per-response where the producer + * guarantees presence — see each schema's note. Optionality is measured, not + * assumed: the six `lockReason` … `packageVersion` keys are spread only when + * `!== undefined` (they read off `_`-prefixed document fields that are + * themselves optional), so they are conditional on EVERY path. + */ +const MetadataProtectionEnvelopeFields = { + lock: MetadataLockSchema.optional().describe( + 'Resolved lock verdict for this item (ADR-0010 §3.3). `none` means unlocked; ' + + '`no-overlay` / `no-delete` / `full` refuse the corresponding write with ' + + '403 `ITEM_LOCKED`. Resolved from the document\'s `_lock`, with the packaged ' + + 'artifact winning over any org overlay.', + ), + lockReason: z.string().optional().describe( + 'Human-readable explanation shown next to a refused write. Present only when ' + + 'the resolved item declares `_lockReason`.', + ), + lockSource: MetadataLockSourceSchema.optional().describe( + 'Which layer asserted the lock. Present only when the resolved item declares ' + + '`_lockSource`.', + ), + lockDocsUrl: z.string().optional().describe( + 'Documentation link surfaced beside `lockReason`. Present only when the ' + + 'resolved item declares `_lockDocsUrl`.', + ), + provenance: MetadataProvenanceSchema.optional().describe( + 'Where the item came from (package | org | env-forced). Present only when the ' + + 'resolved item declares `_provenance`.', + ), + packageId: z.string().optional().describe( + 'Owning package machine id. Present only when the resolved item declares ' + + '`_packageId`.', + ), + packageVersion: z.string().optional().describe( + 'Owning package version. Present only when the resolved item declares ' + + '`_packageVersion`.', + ), + editable: z.boolean().optional().describe( + 'Whether an overlay write is permitted — false iff `lock` is `no-overlay` or ' + + '`full`. A derived verdict: do not recompute it from `lock` client-side.', + ), + deletable: z.boolean().optional().describe( + 'Whether deleting the overlay is permitted — false iff `lock` is `no-delete` ' + + 'or `full`.', + ), + resettable: z.boolean().optional().describe( + 'Whether the item can be reset to its packaged default — true iff it is ' + + 'artifact-backed, i.e. there is a baseline to reset TO.', + ), +} as const; + /** * Get Metadata Item Response + * + * Describes the FULL body `GET /api/v1/meta/:type/:name` can return, not the + * three-key subset it used to claim (#5950 — the read-side twin of the write-side + * gap #5745 closed on {@link SaveMetaItemResponseSchema}). + * + * The declaration stopped at `{ type, name, item }` while the uncached branch + * served ten more keys: the ADR-0010 protection envelope, spread onto the wire + * verbatim by the REST layer (`rest-server.ts`'s `translateMetaEnvelope` does + * `{ ...envelope, item }`). `lock` in particular is the read half of the ADR-0008 + * optimistic-concurrency story the write half already declares — so an SDK caller + * typed against this response could not see it, and reading it meant a cast, the + * consumer-side tolerance this repo rejects by Prime Directive #12. + * + * **Why every protection key is optional, measured rather than assumed.** This + * route reaches a body by two branches and they publish different amounts: + * + * - **cached** (`getMetaItemCached`, THE DEFAULT — `enableCache` defaults to + * `true`): the REST layer rebuilds the envelope as `{ type, name, item }` and + * deliberately resolves NO lock — it is the fast published-value path and + * never consults the lock resolver (`rest-server.ts`, the `cachedEnvelope` + * note). All ten keys are ABSENT. + * - **uncached** (`getMetaItem`): `lock`, `editable`, `deletable` and + * `resettable` are always set; the other six appear only when the resolved + * document carries the corresponding `_`-prefixed field. + * + * So `optional` here means "this deployment/branch did not publish it", NEVER + * "unlocked" — a consumer that needs the OCC carriers must read the uncached + * path and must not read absence as `lock: 'none'`. Declaring them required + * would make the default deployment's own response fail its own contract, which + * is the #5563 defect in mirror image. + * + * ⚠️ This is a DECLARATION change only — zero runtime behaviour is altered. That + * lock presence depends on a server-side cache setting is a separate, larger + * question (#5950 says so explicitly) and is deliberately NOT decided here. */ export const GetMetaItemResponseSchema = lazySchema(() => z.object({ type: z.string().describe('Metadata type name'), name: z.string().describe('Item name'), item: z.unknown().describe('Metadata item definition'), + ...MetadataProtectionEnvelopeFields, +})); + +/** + * Get Metadata Item — LAYERED Response + * + * The body of `GET /api/v1/meta/:type/:name/layers`: a three-layer diagnostic + * projection that shows the packaged baseline, the tenant's customization row + * and the merged result SIDE BY SIDE, which is what drives Studio's + * "code default vs override vs effective" comparison tabs. + * + * **Why this is a separate schema on a separate path** (#5882, ruled B by the + * maintainer 2026-08-06). This projection used to be reached by putting + * `?layers=true` on the ordinary read, so one route answered two unrelated + * resource representations while `packages/spec` declared only one of them — + * anything generating a client from the route table (SDK annotations, codegen, + * an AI-written integration) produced a parser that was simply wrong for the + * flagged call. Collapsing the three layers into + * {@link GetMetaItemResponseSchema}'s single `item` was never an option: seeing + * the layers apart IS the diagnostic. The rejected alternative was teaching the + * route declaration to express "two shapes, chosen by query flag"; that adds a + * new primitive every future tool must understand, and conditional response + * selection is precisely where codegen and AI clients go wrong. One path, one + * response shape — so the projection got its own path. + * + * The `?layers=` flag still answers this same body during its deprecation + * window, marked with `Deprecation` / `Link` response headers. + * + * **Required vs optional, measured against the producer.** Unlike the ordinary + * read there is exactly ONE producer path here (`getMetaItemLayered`; the + * layered view deliberately skips the cache), so the four resolved verdicts + * `lock` / `editable` / `deletable` / `resettable` are ALWAYS set and are + * required below. The six conditional protection keys stay optional for the + * same reason they are optional on the ordinary read. + */ +export const GetMetaItemLayeredResponseSchema = lazySchema(() => z.object({ + type: z.string().describe('Metadata type name (canonical singular)'), + name: z.string().describe('Item name'), + code: z.unknown().describe( + 'LAYER 1 — the packaged artifact baseline exactly as shipped, before any ' + + 'tenant customization. `null` when no artifact ships this item (it exists ' + + 'only as an overlay).', + ), + overlay: z.unknown().describe( + 'LAYER 2 — the stored customization row ALONE, not merged with `code`. ' + + '`null` when this tenant has not customized the item.', + ), + overlayScope: z.enum(['org', 'env']).nullable().describe( + 'Which scope the `overlay` row was read from — `org` for a tenant overlay, ' + + '`env` for an environment-level one. `null` exactly when `overlay` is null.', + ), + effective: z.unknown().describe( + 'LAYER 3 — the merged result, i.e. the value an ordinary ' + + '`GET /meta/:type/:name` would return under `item`. `null` when the item ' + + 'resolves to nothing at all.', + ), + _diagnostics: MetadataValidationResultSchema.optional().describe( + 'Load-time spec-validation verdict for `effective`, so the Studio edit page ' + + 'can raise invalid-metadata banners and inline field errors without a ' + + 'second round trip. ABSENT for metadata types that register no Zod schema ' + + '(function / service / router) — absence means "no opinion", never "valid".', + ), + ...MetadataProtectionEnvelopeFields, + // The four resolved verdicts are unconditional on this single-producer path — + // tightened from the mixin's optional baseline. See the note above. + lock: MetadataLockSchema.describe( + 'Resolved lock verdict (ADR-0010 §3.3), artifact winning over overlay. Always ' + + 'present on this path.', + ), + editable: z.boolean().describe('Whether an overlay write is permitted. Always present on this path.'), + deletable: z.boolean().describe('Whether deleting the overlay is permitted. Always present on this path.'), + resettable: z.boolean().describe('Whether the item can be reset to its packaged default. Always present on this path.'), })); /** @@ -1353,6 +1536,7 @@ export type GetMetaItemsRequest = z.input; export type GetMetaItemsResponse = z.input; export type GetMetaItemRequest = z.input; export type GetMetaItemResponse = z.input; +export type GetMetaItemLayeredResponse = z.input; export type SaveMetaItemRequest = z.input; export type SaveMetaItemResponse = z.input; export type DeleteMetaItemRequest = z.input; diff --git a/packages/spec/src/type-alias-convention.pin.test.ts b/packages/spec/src/type-alias-convention.pin.test.ts index ab15273dfa..96f876e6c0 100644 --- a/packages/spec/src/type-alias-convention.pin.test.ts +++ b/packages/spec/src/type-alias-convention.pin.test.ts @@ -448,6 +448,7 @@ export type Iso131 = Assert, export type Iso132 = Assert, z.infer< typeof M28.GetMetaItemsResponseSchema > >>; export type Iso133 = Assert, z.infer< typeof M28.GetMetaItemRequestSchema > >>; export type Iso134 = Assert, z.infer< typeof M28.GetMetaItemResponseSchema > >>; +export type Iso759 = Assert, z.infer< typeof M28.GetMetaItemLayeredResponseSchema > >>; export type Iso135 = Assert, z.infer< typeof M28.SaveMetaItemRequestSchema > >>; export type Iso136 = Assert, z.infer< typeof M28.SaveMetaItemResponseSchema > >>; export type Iso137 = Assert, z.infer< typeof M28.DeleteMetaItemRequestSchema > >>; @@ -1518,9 +1519,31 @@ describe('ADR-0122 type-alias convention', () => { // from `AggregationFunction`, and an enum VALUE narrowing is invisible // here, exactly as it is to the four surface ratchets. Recompute from the // file; never from the changelog. + // 748 -> 749 is the 2026-08-08 undeclared-response sweep (#6487), the + // OTHER way a count moves: a schema ARRIVED. `GetMetaItemLayeredResponseSchema` + // (#5882) declares the three-layer projection that `GET /meta/:type/:name` + // used to serve, undeclared, behind `?layers=true`. Its tree is enums, + // booleans, `z.unknown()` and `MetadataValidationResultSchema` — no + // `.default()` anywhere — so its two shapes coincide and ADR-0122 gives it a + // pin rather than a `GetMetaItemLayeredResponseParsed` synonym. + // + // Written out per member, because a MULTI-member sweep is exactly where a + // count gets nudged to fit instead of recomputed: + // + // #5882 +1 GetMetaItemLayeredResponseSchema (new schema, isomorphic) + // #5950 0 GetMetaItemResponseSchema was ALREADY pinned (Iso134) and + // stays pinned — it gained ten optional keys, and an enum / + // boolean / string with no default cannot break isomorphism, + // exactly as an enum VALUE narrowing could not in #6486 + // #6442 0 AnalyticsMetadataResponseSchema is not pinned at all; it + // carries an `AnalyticsMetadataResponseParsed` alias instead, + // and narrowing `data` did not change which of the two + // ADR-0122 states it is in + // + // +1, and 748 + 1 = 749. Recompute from the file; never from the changelog. const self = readFileSync(fileURLToPath(import.meta.url), 'utf8'); const pins = self.match(/^export type Iso\d+ = Assert { From 6fab6ec53e5cfbb01d0f5b4400d57144384eddd5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 06:28:16 +0000 Subject: [PATCH 2/4] =?UTF-8?q?fix(spec,rest):=20sweep=20follow-ups=20?= =?UTF-8?q?=E2=80=94=20inline=20the=20analytics=20projection,=20ledger=20t?= =?UTF-8?q?he=20new=20route?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three corrections found by running the gates the sweep touches, none of them a change of direction. ## The analytics projection is inlined rather than separately exported `CubeMetaSchema` / `CubeMetaMemberSchema` were exported named schemas. `gen:docs` rejects a documented schema with no type alias, and the alias those two want is `CubeMeta` — a name `contracts/analytics-service.ts` already owns. Taking it would be both the ADR-0122 D3 permanent synonym and a new dual-source export, and the gate's own prescription for a dual-source finding is "import the existing one, or pick a different name". So the projection is declared inline in `AnalyticsMetadataResponseSchema`, which also matches the style of its neighbour `AnalyticsSqlResponseSchema`. Nothing is lost: `CubeMeta` remains THE name for the shape, and `analytics.test.ts` binds the declaration to it with an exported compile-time assertion, so the two cannot drift. ## The new route needed a reviewed ledger disposition `rest-route-ledger.conformance.test.ts` fails on a mounted route with no entry — the #3587 guard against a working route the SDK cannot call. Added with `disposition: 'server-only'`, deliberately NOT `gap`: the gap count is a ratchet pinned at zero and a new `gap` row is defined to require its own reviewed decision, which this PR does not carry. `server-only` is also accurate on its own terms — `@objectstack/client` expressed no layered read under the `?layers=` spelling either, and Studio consumes the view straight over HTTP — so the row records the status quo under a new path rather than opening or closing a gap. Whether the SDK should express it is a separate product call. Both metadata-item rows now carry a `responseSchema`, which the field's rule permits only where the mount has conformance coverage of its own. That coverage is added here rather than asserted: `meta-item-layered-route.test.ts` parses the body this mount really answers against `GetMetaItemResponseSchema` on BOTH branches — uncached, carrying the newly-declared ADR-0010 envelope, and cached, where every protection key is absent — which is the end-to-end half of #5950 and the reason the envelope is optional rather than required. ## Generated artifacts regenerated wholesale from the merged tree `content/docs/references/api/analytics.mdx` now publishes the array shape instead of the `{ cubes: [...] }` wrapper with `sql` in it, and `protocol.mdx` gains the layered response. `authorable-surface.base.json` is deliberately NOT re-anchored: it trails `1a53a0253356` by keys belonging to unrelated retirements, `check:authorable-surface` is green with it trailing, and the generator itself says re-anchoring is a deliberate act with its own reviewed diff — never a side effect of another PR's build (#5358). --- content/docs/references/api/analytics.mdx | 2 +- content/docs/references/api/protocol.mdx | 41 +++++++++++++++- content/docs/references/index.mdx | 10 ++-- .../rest/src/meta-item-layered-route.test.ts | 31 +++++++++++- packages/rest/src/rest-route-ledger.ts | 22 ++++++++- packages/spec/api-surface/api.json | 2 + packages/spec/authorable-surface/api.json | 7 --- packages/spec/json-schema.manifest/api.json | 2 - packages/spec/src/api/analytics.zod.ts | 47 +++++++------------ 9 files changed, 115 insertions(+), 49 deletions(-) diff --git a/content/docs/references/api/analytics.mdx b/content/docs/references/api/analytics.mdx index 11e2e4b55f..671ff7a92a 100644 --- a/content/docs/references/api/analytics.mdx +++ b/content/docs/references/api/analytics.mdx @@ -46,7 +46,7 @@ const result = AnalyticsEndpoint.parse(data); | **success** | `boolean` | ✅ | Operation success status | | **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +257 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | -| **data** | `{ cubes: { name: string; title?: string; description?: string; sql: string; … }[] }` | ✅ | | +| **data** | `{ name: string; title?: string; measures: { name: string; type: string; title?: string }[]; dimensions: { name: string; type: string; title?: string }[] }[]` | ✅ | Available cubes, each as the `CubeMeta` discovery projection — the cube name, its title, and the measures/dimensions a client may name in a query. A bare array: there is no `cubes` wrapper object, and no cube `sql` is published. | --- diff --git a/content/docs/references/api/protocol.mdx b/content/docs/references/api/protocol.mdx index 4aaae09b9f..1024f442b3 100644 --- a/content/docs/references/api/protocol.mdx +++ b/content/docs/references/api/protocol.mdx @@ -12,8 +12,8 @@ description: Protocol protocol schemas ## TypeScript Usage ```typescript -import { AiAgentCapabilitiesSchema, AiAgentChatRequestSchema, AiAgentSummarySchema, AiAgentsResponseSchema, AiChatRequestSchema, AiChatResponseSchema, AiCompleteRequestSchema, AiConversationSchema, AiMessageSchema, AiModelsResponseSchema, AiPendingActionSchema, AiPendingActionStatusSchema, AiStreamChunkSchema, ApproveAiPendingActionResponseSchema, AutomationActionsResponseSchema, AutomationTriggerRequestSchema, AutomationTriggerResponseSchema, BatchDataRequestSchema, BatchDataResponseSchema, CheckPermissionRequestSchema, CheckPermissionResponseSchema, CreateAiConversationRequestSchema, CreateDataRequestSchema, CreateDataResponseSchema, CreateManyDataRequestSchema, CreateManyDataResponseSchema, DeleteDataRequestSchema, DeleteDataResponseSchema, DeleteManyDataRequestSchema, DeleteManyDataResponseSchema, DeleteMetaItemRequestSchema, DeleteMetaItemResponseSchema, DisablePackageRequestSchema, DisablePackageResponseSchema, EnablePackageRequestSchema, EnablePackageResponseSchema, FindDataRequestSchema, FindDataResponseSchema, GetDataRequestSchema, GetDataResponseSchema, GetDiscoveryRequestSchema, GetDiscoveryResponseSchema, GetEffectivePermissionsRequestSchema, GetEffectivePermissionsResponseSchema, GetFieldLabelsRequestSchema, GetFieldLabelsResponseSchema, GetLocalesRequestSchema, GetLocalesResponseSchema, GetMetaItemCachedRequestSchema, GetMetaItemCachedResponseSchema, GetMetaItemRequestSchema, GetMetaItemResponseSchema, GetMetaItemsRequestSchema, GetMetaItemsResponseSchema, GetMetaTypesRequestSchema, GetMetaTypesResponseSchema, GetNotificationPreferencesRequestSchema, GetNotificationPreferencesResponseSchema, GetObjectPermissionsRequestSchema, GetObjectPermissionsResponseSchema, GetPackageRequestSchema, GetPackageResponseSchema, GetPresenceRequestSchema, GetPresenceResponseSchema, GetTranslationsRequestSchema, GetTranslationsResponseSchema, GetUiViewRequestSchema, GetUiViewResponseSchema, HttpFindQueryParamsSchema, InstallPackageRequestSchema, InstallPackageResponseSchema, ListAiConversationsRequestSchema, ListAiConversationsResponseSchema, ListAiPendingActionsRequestSchema, ListAiPendingActionsResponseSchema, ListNotificationsRequestSchema, ListNotificationsResponseSchema, ListPackagesRequestSchema, ListPackagesResponseSchema, MarkAllNotificationsReadRequestSchema, MarkAllNotificationsReadResponseSchema, MarkNotificationsReadRequestSchema, MarkNotificationsReadResponseSchema, NotificationSchema, NotificationPreferencesSchema, RealtimeConnectRequestSchema, RealtimeConnectResponseSchema, RealtimeDisconnectRequestSchema, RealtimeDisconnectResponseSchema, RealtimeSubscribeRequestSchema, RealtimeSubscribeResponseSchema, RealtimeUnsubscribeRequestSchema, RealtimeUnsubscribeResponseSchema, RegisterDeviceRequestSchema, RegisterDeviceResponseSchema, RejectAiPendingActionResponseSchema, SaveMetaItemRequestSchema, SaveMetaItemResponseSchema, SetPresenceRequestSchema, SetPresenceResponseSchema, UninstallPackageRequestSchema, UninstallPackageResponseSchema, UnregisterDeviceRequestSchema, UnregisterDeviceResponseSchema, UpdateAiConversationRequestSchema, UpdateDataRequestSchema, UpdateDataResponseSchema, UpdateManyDataRequestSchema, UpdateManyDataResponseSchema, UpdateNotificationPreferencesRequestSchema, UpdateNotificationPreferencesResponseSchema, ValidateDataIssueSchema, ValidateDataRequestSchema, ValidateDataResponseSchema } from '@objectstack/spec/api'; -import type { AiAgentCapabilities, AiAgentChatRequest, AiAgentSummary, AiAgentsResponse, AiChatRequest, AiChatResponse, AiCompleteRequest, AiConversation, AiMessage, AiModelsResponse, AiPendingAction, AiPendingActionStatus, AiStreamChunk, ApproveAiPendingActionResponse, AutomationActionsResponse, AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CheckPermissionRequest, CheckPermissionResponse, CreateAiConversationRequest, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, DeleteMetaItemRequest, DeleteMetaItemResponse, DisablePackageRequest, DisablePackageResponse, EnablePackageRequest, EnablePackageResponse, FindDataRequest, FindDataResponse, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetEffectivePermissionsRequest, GetEffectivePermissionsResponse, GetFieldLabelsRequest, GetFieldLabelsResponse, GetLocalesRequest, GetLocalesResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetObjectPermissionsRequest, GetObjectPermissionsResponse, GetPackageRequest, GetPackageResponse, GetPresenceRequest, GetPresenceResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetUiViewResponse, InstallPackageRequest, InstallPackageResponse, ListAiConversationsRequest, ListAiConversationsResponse, ListAiPendingActionsRequest, ListAiPendingActionsResponse, ListNotificationsRequest, ListNotificationsResponse, ListPackagesRequest, ListPackagesResponse, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, Notification, NotificationPreferences, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, RejectAiPendingActionResponse, SaveMetaItemRequest, SaveMetaItemResponse, SetPresenceRequest, SetPresenceResponse, UninstallPackageRequest, UninstallPackageResponse, UnregisterDeviceRequest, UnregisterDeviceResponse, UpdateAiConversationRequest, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse, UpdateNotificationPreferencesRequest, UpdateNotificationPreferencesResponse, ValidateDataIssue, ValidateDataRequest, ValidateDataResponse } from '@objectstack/spec/api'; +import { AiAgentCapabilitiesSchema, AiAgentChatRequestSchema, AiAgentSummarySchema, AiAgentsResponseSchema, AiChatRequestSchema, AiChatResponseSchema, AiCompleteRequestSchema, AiConversationSchema, AiMessageSchema, AiModelsResponseSchema, AiPendingActionSchema, AiPendingActionStatusSchema, AiStreamChunkSchema, ApproveAiPendingActionResponseSchema, AutomationActionsResponseSchema, AutomationTriggerRequestSchema, AutomationTriggerResponseSchema, BatchDataRequestSchema, BatchDataResponseSchema, CheckPermissionRequestSchema, CheckPermissionResponseSchema, CreateAiConversationRequestSchema, CreateDataRequestSchema, CreateDataResponseSchema, CreateManyDataRequestSchema, CreateManyDataResponseSchema, DeleteDataRequestSchema, DeleteDataResponseSchema, DeleteManyDataRequestSchema, DeleteManyDataResponseSchema, DeleteMetaItemRequestSchema, DeleteMetaItemResponseSchema, DisablePackageRequestSchema, DisablePackageResponseSchema, EnablePackageRequestSchema, EnablePackageResponseSchema, FindDataRequestSchema, FindDataResponseSchema, GetDataRequestSchema, GetDataResponseSchema, GetDiscoveryRequestSchema, GetDiscoveryResponseSchema, GetEffectivePermissionsRequestSchema, GetEffectivePermissionsResponseSchema, GetFieldLabelsRequestSchema, GetFieldLabelsResponseSchema, GetLocalesRequestSchema, GetLocalesResponseSchema, GetMetaItemCachedRequestSchema, GetMetaItemCachedResponseSchema, GetMetaItemLayeredResponseSchema, GetMetaItemRequestSchema, GetMetaItemResponseSchema, GetMetaItemsRequestSchema, GetMetaItemsResponseSchema, GetMetaTypesRequestSchema, GetMetaTypesResponseSchema, GetNotificationPreferencesRequestSchema, GetNotificationPreferencesResponseSchema, GetObjectPermissionsRequestSchema, GetObjectPermissionsResponseSchema, GetPackageRequestSchema, GetPackageResponseSchema, GetPresenceRequestSchema, GetPresenceResponseSchema, GetTranslationsRequestSchema, GetTranslationsResponseSchema, GetUiViewRequestSchema, GetUiViewResponseSchema, HttpFindQueryParamsSchema, InstallPackageRequestSchema, InstallPackageResponseSchema, ListAiConversationsRequestSchema, ListAiConversationsResponseSchema, ListAiPendingActionsRequestSchema, ListAiPendingActionsResponseSchema, ListNotificationsRequestSchema, ListNotificationsResponseSchema, ListPackagesRequestSchema, ListPackagesResponseSchema, MarkAllNotificationsReadRequestSchema, MarkAllNotificationsReadResponseSchema, MarkNotificationsReadRequestSchema, MarkNotificationsReadResponseSchema, NotificationSchema, NotificationPreferencesSchema, RealtimeConnectRequestSchema, RealtimeConnectResponseSchema, RealtimeDisconnectRequestSchema, RealtimeDisconnectResponseSchema, RealtimeSubscribeRequestSchema, RealtimeSubscribeResponseSchema, RealtimeUnsubscribeRequestSchema, RealtimeUnsubscribeResponseSchema, RegisterDeviceRequestSchema, RegisterDeviceResponseSchema, RejectAiPendingActionResponseSchema, SaveMetaItemRequestSchema, SaveMetaItemResponseSchema, SetPresenceRequestSchema, SetPresenceResponseSchema, UninstallPackageRequestSchema, UninstallPackageResponseSchema, UnregisterDeviceRequestSchema, UnregisterDeviceResponseSchema, UpdateAiConversationRequestSchema, UpdateDataRequestSchema, UpdateDataResponseSchema, UpdateManyDataRequestSchema, UpdateManyDataResponseSchema, UpdateNotificationPreferencesRequestSchema, UpdateNotificationPreferencesResponseSchema, ValidateDataIssueSchema, ValidateDataRequestSchema, ValidateDataResponseSchema } from '@objectstack/spec/api'; +import type { AiAgentCapabilities, AiAgentChatRequest, AiAgentSummary, AiAgentsResponse, AiChatRequest, AiChatResponse, AiCompleteRequest, AiConversation, AiMessage, AiModelsResponse, AiPendingAction, AiPendingActionStatus, AiStreamChunk, ApproveAiPendingActionResponse, AutomationActionsResponse, AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CheckPermissionRequest, CheckPermissionResponse, CreateAiConversationRequest, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, DeleteMetaItemRequest, DeleteMetaItemResponse, DisablePackageRequest, DisablePackageResponse, EnablePackageRequest, EnablePackageResponse, FindDataRequest, FindDataResponse, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetEffectivePermissionsRequest, GetEffectivePermissionsResponse, GetFieldLabelsRequest, GetFieldLabelsResponse, GetLocalesRequest, GetLocalesResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemLayeredResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetObjectPermissionsRequest, GetObjectPermissionsResponse, GetPackageRequest, GetPackageResponse, GetPresenceRequest, GetPresenceResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetUiViewResponse, InstallPackageRequest, InstallPackageResponse, ListAiConversationsRequest, ListAiConversationsResponse, ListAiPendingActionsRequest, ListAiPendingActionsResponse, ListNotificationsRequest, ListNotificationsResponse, ListPackagesRequest, ListPackagesResponse, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, Notification, NotificationPreferences, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, RejectAiPendingActionResponse, SaveMetaItemRequest, SaveMetaItemResponse, SetPresenceRequest, SetPresenceResponse, UninstallPackageRequest, UninstallPackageResponse, UnregisterDeviceRequest, UnregisterDeviceResponse, UpdateAiConversationRequest, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse, UpdateNotificationPreferencesRequest, UpdateNotificationPreferencesResponse, ValidateDataIssue, ValidateDataRequest, ValidateDataResponse } from '@objectstack/spec/api'; // Validate data const result = AiAgentCapabilitiesSchema.parse(data); @@ -699,6 +699,33 @@ Enable package response | **version** | `string` | optional | Metadata version identifier | +--- + +## GetMetaItemLayeredResponse + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `string` | ✅ | Metadata type name (canonical singular) | +| **name** | `string` | ✅ | Item name | +| **code** | `any` | ✅ | LAYER 1 — the packaged artifact baseline exactly as shipped, before any tenant customization. `null` when no artifact ships this item (it exists only as an overlay). | +| **overlay** | `any` | ✅ | LAYER 2 — the stored customization row ALONE, not merged with `code`. `null` when this tenant has not customized the item. | +| **overlayScope** | `Enum<'org' \| 'env'> \| null` | ✅ | Which scope the `overlay` row was read from — `org` for a tenant overlay, `env` for an environment-level one. `null` exactly when `overlay` is null. | +| **effective** | `any` | ✅ | LAYER 3 — the merged result, i.e. the value an ordinary `GET /meta/:type/:name` would return under `item`. `null` when the item resolves to nothing at all. | +| **_diagnostics** | `{ valid: boolean; errors?: { path: string; message: string; code?: string }[]; warnings?: { path: string; message: string }[] }` | optional | Load-time spec-validation verdict for `effective`, so the Studio edit page can raise invalid-metadata banners and inline field errors without a second round trip. ABSENT for metadata types that register no Zod schema (function / service / router) — absence means "no opinion", never "valid". | +| **lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | ✅ | Resolved lock verdict (ADR-0010 §3.3), artifact winning over overlay. Always present on this path. | +| **lockReason** | `string` | optional | Human-readable explanation shown next to a refused write. Present only when the resolved item declares `_lockReason`. | +| **lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Which layer asserted the lock. Present only when the resolved item declares `_lockSource`. | +| **lockDocsUrl** | `string` | optional | Documentation link surfaced beside `lockReason`. Present only when the resolved item declares `_lockDocsUrl`. | +| **provenance** | `Enum<'package' \| 'org' \| 'env-forced'>` | optional | Where the item came from (package \| org \| env-forced). Present only when the resolved item declares `_provenance`. | +| **packageId** | `string` | optional | Owning package machine id. Present only when the resolved item declares `_packageId`. | +| **packageVersion** | `string` | optional | Owning package version. Present only when the resolved item declares `_packageVersion`. | +| **editable** | `boolean` | ✅ | Whether an overlay write is permitted. Always present on this path. | +| **deletable** | `boolean` | ✅ | Whether deleting the overlay is permitted. Always present on this path. | +| **resettable** | `boolean` | ✅ | Whether the item can be reset to its packaged default. Always present on this path. | + + --- ## GetMetaItemRequest @@ -723,6 +750,16 @@ Enable package response | **type** | `string` | ✅ | Metadata type name | | **name** | `string` | ✅ | Item name | | **item** | `any` | ✅ | Metadata item definition | +| **lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Resolved lock verdict for this item (ADR-0010 §3.3). `none` means unlocked; `no-overlay` / `no-delete` / `full` refuse the corresponding write with 403 `ITEM_LOCKED`. Resolved from the document's `_lock`, with the packaged artifact winning over any org overlay. | +| **lockReason** | `string` | optional | Human-readable explanation shown next to a refused write. Present only when the resolved item declares `_lockReason`. | +| **lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Which layer asserted the lock. Present only when the resolved item declares `_lockSource`. | +| **lockDocsUrl** | `string` | optional | Documentation link surfaced beside `lockReason`. Present only when the resolved item declares `_lockDocsUrl`. | +| **provenance** | `Enum<'package' \| 'org' \| 'env-forced'>` | optional | Where the item came from (package \| org \| env-forced). Present only when the resolved item declares `_provenance`. | +| **packageId** | `string` | optional | Owning package machine id. Present only when the resolved item declares `_packageId`. | +| **packageVersion** | `string` | optional | Owning package version. Present only when the resolved item declares `_packageVersion`. | +| **editable** | `boolean` | optional | Whether an overlay write is permitted — false iff `lock` is `no-overlay` or `full`. A derived verdict: do not recompute it from `lock` client-side. | +| **deletable** | `boolean` | optional | Whether deleting the overlay is permitted — false iff `lock` is `no-delete` or `full`. | +| **resettable** | `boolean` | optional | Whether the item can be reset to its packaged default — true iff it is artifact-backed, i.e. there is a baseline to reset TO. | --- diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx index 7ba22b6797..1c7b674b3b 100644 --- a/content/docs/references/index.mdx +++ b/content/docs/references/index.mdx @@ -1,6 +1,6 @@ --- title: Protocol Reference -description: Every schema published by @objectstack/spec — 1582 schemas across 14 protocol modules +description: Every schema published by @objectstack/spec — 1583 schemas across 14 protocol modules --- {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} @@ -20,7 +20,7 @@ counts are sums of the rows they head. Regenerate with | Module | Pages | Schemas | Description | | :--- | ---: | ---: | :--- | | [AI Protocol](/docs/references/ai) | 11 | 66 | Agents, tools, skills, RAG and knowledge sources, model registry, conversations. | -| [API Protocol](/docs/references/api) | 28 | 409 | REST/GraphQL contracts, endpoints, routing, realtime, batch, discovery. | +| [API Protocol](/docs/references/api) | 28 | 410 | REST/GraphQL contracts, endpoints, routing, realtime, batch, discovery. | | [Automation Protocol](/docs/references/automation) | 13 | 68 | Flows and their nodes, approvals, ETL pipelines, webhooks, state machines, execution records. | | [Cloud Protocol](/docs/references/cloud) | 11 | 94 | Environments, packages and versions, marketplace, developer portal, tenancy. | | [Data Protocol](/docs/references/data) | 29 | 164 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. | @@ -33,7 +33,7 @@ counts are sums of the rows they head. Regenerate with | [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. | | [System Protocol](/docs/references/system) | 37 | 292 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. | | [UI Protocol](/docs/references/ui) | 16 | 146 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | -| **Total** | **199** | **1582** | 14 protocol modules | +| **Total** | **199** | **1583** | 14 protocol modules | --- @@ -61,7 +61,7 @@ Agents, tools, skills, RAG and knowledge sources, model registry, conversations. ## API Protocol -**Source:** `packages/spec/src/api/` · **Import:** `@objectstack/spec/api` · **28 pages, 409 schemas** +**Source:** `packages/spec/src/api/` · **Import:** `@objectstack/spec/api` · **28 pages, 410 schemas** REST/GraphQL contracts, endpoints, routing, realtime, batch, discovery. @@ -86,7 +86,7 @@ REST/GraphQL contracts, endpoints, routing, realtime, batch, discovery. | [`odata.zod.ts`](/docs/references/api/odata) | `ODataConfig`, `ODataError`, `ODataFilterFunction`, `ODataMetadata`, `ODataQuery`, `ODataResponse` | | [`package-api.zod.ts`](/docs/references/api/package-api) | `GetInstalledPackageRequest`, `GetInstalledPackageResponse`, `ListInstalledPackagesRequest`, `ListInstalledPackagesResponse`, `PackageApiErrorCode`, `PackageInstallRequest`, `PackageInstallResponse`, `PackagePathParams`, `PackageRollbackRequest`, `PackageRollbackResponse`, `PackageUpgradeRequest`, `PackageUpgradeResponse`, `ResolveDependenciesRequest`, `ResolveDependenciesResponse`, `UninstallPackageApiRequest`, `UninstallPackageApiResponse`, `UploadArtifactRequest`, `UploadArtifactResponse` | | [`plugin-rest-api.zod.ts`](/docs/references/api/plugin-rest-api) | `ErrorHandlingConfig`, `HandlerStatus`, `OpenApiGenerationConfig`, `RequestValidationConfig`, `ResponseEnvelopeConfig`, `RestApiEndpoint`, `RestApiPluginConfig`, `RestApiRouteCategory`, `RestApiRouteRegistration`, `RouteCoverageEntry`, `RouteCoverageReport`, `ValidationMode` | -| [`protocol.zod.ts`](/docs/references/api/protocol) | `AiAgentCapabilities`, `AiAgentChatRequest`, `AiAgentSummary`, `AiAgentsResponse`, `AiChatRequest`, `AiChatResponse`, `AiCompleteRequest`, `AiConversation`, `AiMessage`, `AiModelsResponse`, `AiPendingAction`, `AiPendingActionStatus`, `AiStreamChunk`, `ApproveAiPendingActionResponse`, `AutomationActionsResponse`, `AutomationTriggerRequest`, `AutomationTriggerResponse`, `BatchDataRequest`, `BatchDataResponse`, `CheckPermissionRequest`, `CheckPermissionResponse`, `CreateAiConversationRequest`, `CreateDataRequest`, `CreateDataResponse`, `CreateManyDataRequest`, `CreateManyDataResponse`, `DeleteDataRequest`, `DeleteDataResponse`, `DeleteManyDataRequest`, `DeleteManyDataResponse`, `DeleteMetaItemRequest`, `DeleteMetaItemResponse`, `DisablePackageRequest`, `DisablePackageResponse`, `EnablePackageRequest`, `EnablePackageResponse`, `FindDataRequest`, `FindDataResponse`, `GetDataRequest`, `GetDataResponse`, `GetDiscoveryRequest`, `GetDiscoveryResponse`, `GetEffectivePermissionsRequest`, `GetEffectivePermissionsResponse`, `GetFieldLabelsRequest`, `GetFieldLabelsResponse`, `GetLocalesRequest`, `GetLocalesResponse`, `GetMetaItemCachedRequest`, `GetMetaItemCachedResponse`, `GetMetaItemRequest`, `GetMetaItemResponse`, `GetMetaItemsRequest`, `GetMetaItemsResponse`, `GetMetaTypesRequest`, `GetMetaTypesResponse`, `GetNotificationPreferencesRequest`, `GetNotificationPreferencesResponse`, `GetObjectPermissionsRequest`, `GetObjectPermissionsResponse`, `GetPackageRequest`, `GetPackageResponse`, `GetPresenceRequest`, `GetPresenceResponse`, `GetTranslationsRequest`, `GetTranslationsResponse`, `GetUiViewRequest`, `GetUiViewResponse`, `HttpFindQueryParams`, `InstallPackageRequest`, `InstallPackageResponse`, `ListAiConversationsRequest`, `ListAiConversationsResponse`, `ListAiPendingActionsRequest`, `ListAiPendingActionsResponse`, `ListNotificationsRequest`, `ListNotificationsResponse`, `ListPackagesRequest`, `ListPackagesResponse`, `MarkAllNotificationsReadRequest`, `MarkAllNotificationsReadResponse`, `MarkNotificationsReadRequest`, `MarkNotificationsReadResponse`, `Notification`, `NotificationPreferences`, `RealtimeConnectRequest`, `RealtimeConnectResponse`, `RealtimeDisconnectRequest`, `RealtimeDisconnectResponse`, `RealtimeSubscribeRequest`, `RealtimeSubscribeResponse`, `RealtimeUnsubscribeRequest`, `RealtimeUnsubscribeResponse`, `RegisterDeviceRequest`, `RegisterDeviceResponse`, `RejectAiPendingActionResponse`, `SaveMetaItemRequest`, `SaveMetaItemResponse`, `SetPresenceRequest`, `SetPresenceResponse`, `UninstallPackageRequest`, `UninstallPackageResponse`, `UnregisterDeviceRequest`, `UnregisterDeviceResponse`, `UpdateAiConversationRequest`, `UpdateDataRequest`, `UpdateDataResponse`, `UpdateManyDataRequest`, `UpdateManyDataResponse`, `UpdateNotificationPreferencesRequest`, `UpdateNotificationPreferencesResponse`, `ValidateDataIssue`, `ValidateDataRequest`, `ValidateDataResponse` | +| [`protocol.zod.ts`](/docs/references/api/protocol) | `AiAgentCapabilities`, `AiAgentChatRequest`, `AiAgentSummary`, `AiAgentsResponse`, `AiChatRequest`, `AiChatResponse`, `AiCompleteRequest`, `AiConversation`, `AiMessage`, `AiModelsResponse`, `AiPendingAction`, `AiPendingActionStatus`, `AiStreamChunk`, `ApproveAiPendingActionResponse`, `AutomationActionsResponse`, `AutomationTriggerRequest`, `AutomationTriggerResponse`, `BatchDataRequest`, `BatchDataResponse`, `CheckPermissionRequest`, `CheckPermissionResponse`, `CreateAiConversationRequest`, `CreateDataRequest`, `CreateDataResponse`, `CreateManyDataRequest`, `CreateManyDataResponse`, `DeleteDataRequest`, `DeleteDataResponse`, `DeleteManyDataRequest`, `DeleteManyDataResponse`, `DeleteMetaItemRequest`, `DeleteMetaItemResponse`, `DisablePackageRequest`, `DisablePackageResponse`, `EnablePackageRequest`, `EnablePackageResponse`, `FindDataRequest`, `FindDataResponse`, `GetDataRequest`, `GetDataResponse`, `GetDiscoveryRequest`, `GetDiscoveryResponse`, `GetEffectivePermissionsRequest`, `GetEffectivePermissionsResponse`, `GetFieldLabelsRequest`, `GetFieldLabelsResponse`, `GetLocalesRequest`, `GetLocalesResponse`, `GetMetaItemCachedRequest`, `GetMetaItemCachedResponse`, `GetMetaItemLayeredResponse`, `GetMetaItemRequest`, `GetMetaItemResponse`, `GetMetaItemsRequest`, `GetMetaItemsResponse`, `GetMetaTypesRequest`, `GetMetaTypesResponse`, `GetNotificationPreferencesRequest`, `GetNotificationPreferencesResponse`, `GetObjectPermissionsRequest`, `GetObjectPermissionsResponse`, `GetPackageRequest`, `GetPackageResponse`, `GetPresenceRequest`, `GetPresenceResponse`, `GetTranslationsRequest`, `GetTranslationsResponse`, `GetUiViewRequest`, `GetUiViewResponse`, `HttpFindQueryParams`, `InstallPackageRequest`, `InstallPackageResponse`, `ListAiConversationsRequest`, `ListAiConversationsResponse`, `ListAiPendingActionsRequest`, `ListAiPendingActionsResponse`, `ListNotificationsRequest`, `ListNotificationsResponse`, `ListPackagesRequest`, `ListPackagesResponse`, `MarkAllNotificationsReadRequest`, `MarkAllNotificationsReadResponse`, `MarkNotificationsReadRequest`, `MarkNotificationsReadResponse`, `Notification`, `NotificationPreferences`, `RealtimeConnectRequest`, `RealtimeConnectResponse`, `RealtimeDisconnectRequest`, `RealtimeDisconnectResponse`, `RealtimeSubscribeRequest`, `RealtimeSubscribeResponse`, `RealtimeUnsubscribeRequest`, `RealtimeUnsubscribeResponse`, `RegisterDeviceRequest`, `RegisterDeviceResponse`, `RejectAiPendingActionResponse`, `SaveMetaItemRequest`, `SaveMetaItemResponse`, `SetPresenceRequest`, `SetPresenceResponse`, `UninstallPackageRequest`, `UninstallPackageResponse`, `UnregisterDeviceRequest`, `UnregisterDeviceResponse`, `UpdateAiConversationRequest`, `UpdateDataRequest`, `UpdateDataResponse`, `UpdateManyDataRequest`, `UpdateManyDataResponse`, `UpdateNotificationPreferencesRequest`, `UpdateNotificationPreferencesResponse`, `ValidateDataIssue`, `ValidateDataRequest`, `ValidateDataResponse` | | [`query-adapter.zod.ts`](/docs/references/api/query-adapter) | `ODataQueryAdapter`, `OperatorMapping`, `QueryAdapterConfig`, `QueryAdapterTarget`, `RestQueryAdapter` | | [`realtime.zod.ts`](/docs/references/api/realtime) | `RealtimeConfig`, `RealtimeEvent`, `RealtimeEventType`, `RealtimePresence`, `Subscription`, `SubscriptionEvent`, `TransportProtocol` | | [`realtime-shared.zod.ts`](/docs/references/api/realtime-shared) | `BasePresence`, `PresenceStatus`, `RealtimeRecordAction` | diff --git a/packages/rest/src/meta-item-layered-route.test.ts b/packages/rest/src/meta-item-layered-route.test.ts index 2e92f43f8b..8f4af1c47f 100644 --- a/packages/rest/src/meta-item-layered-route.test.ts +++ b/packages/rest/src/meta-item-layered-route.test.ts @@ -33,7 +33,7 @@ import { describe, it, expect, vi } from 'vitest'; import { RestServer } from './rest-server'; -import { GetMetaItemLayeredResponseSchema } from '@objectstack/spec/api'; +import { GetMetaItemLayeredResponseSchema, GetMetaItemResponseSchema } from '@objectstack/spec/api'; const ANON_API = { api: { requireAuth: false } }; @@ -225,6 +225,35 @@ describe('#5882 the ordinary read is undisturbed', () => { expect(body.effective).toBeUndefined(); }); + it('answers a body that parses against `GetMetaItemResponseSchema` on BOTH branches (#5950)', async () => { + // The end-to-end half of #5950, and what entitles this route's ledger + // row to name a `responseSchema`: the declaration is measured against + // the real mount, on both branches that reach a body, rather than + // against a schema-level fixture that no handler produced. + + // Uncached: carries the full ADR-0010 protection envelope. + const uncached = await dispatch(baseProtocol(), ITEM_PATH, { type: 'object', name: 'customer' }); + const uncachedParse = GetMetaItemResponseSchema.safeParse(uncached.body); + expect(uncachedParse.error?.issues ?? []).toEqual([]); + // The carriers survive the parse — the point of the member. Before the + // declaration widened, `.parse()` dropped every one of them. + expect((uncachedParse.data as any).lock).toBe('none'); + expect((uncachedParse.data as any).editable).toBe(true); + + // Cached (THE DEFAULT): three keys, no lock resolved — still valid, + // which is why the envelope is declared optional rather than required. + const cachedProtocol = baseProtocol({ + getMetaItemCached: vi.fn().mockResolvedValue({ + data: CUSTOMER, etag: { value: 'abc', weak: false }, notModified: false, + }), + }); + const cached = await dispatch(cachedProtocol, ITEM_PATH, { type: 'object', name: 'customer' }); + expect(cachedProtocol.getMetaItemCached).toHaveBeenCalled(); + const cachedParse = GetMetaItemResponseSchema.safeParse(cached.body); + expect(cachedParse.error?.issues ?? []).toEqual([]); + expect((cachedParse.data as any).lock).toBeUndefined(); + }); + it('treats `?layers=` with an empty value as NOT a layered request', async () => { // Pre-existing semantics, pinned so the new route does not quietly // change which requests are layered. diff --git a/packages/rest/src/rest-route-ledger.ts b/packages/rest/src/rest-route-ledger.ts index eafa9189d7..8256b9413c 100644 --- a/packages/rest/src/rest-route-ledger.ts +++ b/packages/rest/src/rest-route-ledger.ts @@ -136,7 +136,27 @@ export const REST_ROUTE_LEDGER: readonly RestRouteLedgerEntry[] = [ { route: 'GET /api/v1/meta/:type', family: 'metadata', source: 'route-manager', disposition: 'sdk', client: 'meta.getItems' }, { route: 'GET /api/v1/meta/:type/:name/references', family: 'metadata', source: 'route-manager', disposition: 'sdk', client: 'meta.getReferences' }, { route: 'GET /api/v1/meta/book/:name/tree', family: 'metadata', source: 'route-manager', disposition: 'sdk', client: 'meta.getBookTree' }, - { route: 'GET /api/v1/meta/:type/:name', family: 'metadata', source: 'route-manager', disposition: 'sdk', client: 'meta.getItem' }, + // [#5882] The three-layer diagnostic projection, promoted from the + // `?layers=true` flag on the row below to a path of its own so that one path + // answers one response shape. `responseSchema` is filled because this mount + // HAS conformance coverage of its own: `meta-item-layered-route.test.ts` + // drives this handler and parses the body it answers against the named + // schema — not "same handler, therefore same shape". + // + // `server-only`, and NOT `gap`: the gap ratchet is pinned at zero and a new + // `gap` row is defined to need its own reviewed decision, which this PR does + // not carry. The disposition is accurate on its own terms — the SDK has never + // expressed a layered read, the `?layers=` spelling this path replaces was + // equally unreachable through `@objectstack/client`, and Studio consumes it + // straight over HTTP. So this row opens no gap and closes none; it records the + // status quo under a new path. Whether the SDK SHOULD express it is a separate + // product call. + { route: 'GET /api/v1/meta/:type/:name/layers', family: 'metadata', source: 'route-manager', disposition: 'server-only', + responseSchema: 'GetMetaItemLayeredResponseSchema', + note: 'three-layer diagnostic read (code / overlay / effective) powering the Studio editor comparison tabs; consumed by objectui over plain HTTP, and the SDK expressed no layered read under the `?layers=` spelling either. Answers BARE, so the named schema is the whole body' }, + { route: 'GET /api/v1/meta/:type/:name', family: 'metadata', source: 'route-manager', disposition: 'sdk', client: 'meta.getItem', + responseSchema: 'GetMetaItemResponseSchema', + note: '[#5950] answers BARE, so the named schema is the whole body. Filled now that meta-item-layered-route.test.ts parses BOTH branches of this mount (cached and uncached) against it — the uncached branch carries the ADR-0010 protection envelope this schema newly declares' }, { route: 'PUT /api/v1/meta/:type/:name', family: 'metadata', source: 'route-manager', disposition: 'sdk', client: 'meta.saveItem' }, { route: 'DELETE /api/v1/meta/:type/:name', family: 'metadata', source: 'route-manager', disposition: 'sdk', client: 'meta.deleteItem', note: 'REST-only: the dispatcher /meta branch has no DELETE handling — it falls into the read path' }, diff --git a/packages/spec/api-surface/api.json b/packages/spec/api-surface/api.json index ee12fb522e..a9b23a0648 100644 --- a/packages/spec/api-surface/api.json +++ b/packages/spec/api-surface/api.json @@ -412,6 +412,8 @@ "GetMetaItemCachedResponse (type)", "GetMetaItemCachedResponseParsed (type)", "GetMetaItemCachedResponseSchema (const)", + "GetMetaItemLayeredResponse (type)", + "GetMetaItemLayeredResponseSchema (const)", "GetMetaItemRequest (type)", "GetMetaItemRequestSchema (const)", "GetMetaItemResponse (type)", diff --git a/packages/spec/authorable-surface/api.json b/packages/spec/authorable-surface/api.json index 6d7981d3c4..9f4087270b 100644 --- a/packages/spec/authorable-surface/api.json +++ b/packages/spec/authorable-surface/api.json @@ -411,13 +411,6 @@ "api/CrudEndpointsConfig:objectParamStyle", "api/CrudEndpointsConfig:operations", "api/CrudEndpointsConfig:patterns", - "api/CubeMeta:dimensions", - "api/CubeMeta:measures", - "api/CubeMeta:name", - "api/CubeMeta:title", - "api/CubeMetaMember:name", - "api/CubeMetaMember:title", - "api/CubeMetaMember:type", "api/CursorMessage:cursor", "api/CursorMessage:messageId", "api/CursorMessage:timestamp", diff --git a/packages/spec/json-schema.manifest/api.json b/packages/spec/json-schema.manifest/api.json index 08ea46d28f..83e3e373ee 100644 --- a/packages/spec/json-schema.manifest/api.json +++ b/packages/spec/json-schema.manifest/api.json @@ -93,8 +93,6 @@ "api/CrudEndpointPattern", "api/CrudEndpointsConfig", "api/CrudOperation", - "api/CubeMeta", - "api/CubeMetaMember", "api/CursorMessage", "api/CursorPosition", "api/DataEvent", diff --git a/packages/spec/src/api/analytics.zod.ts b/packages/spec/src/api/analytics.zod.ts index bfec7e04b5..dccca11f8d 100644 --- a/packages/spec/src/api/analytics.zod.ts +++ b/packages/spec/src/api/analytics.zod.ts @@ -88,8 +88,8 @@ export const GetAnalyticsMetaRequestSchema = lazySchema(() => z.object({ })); /** - * A measure or dimension as `GET /analytics/meta` publishes it — the - * discovery projection, not the authoring definition. + * A measure or dimension as `GET /analytics/meta` publishes it — the discovery + * projection, not the authoring definition. * * `name` is CUBE-QUALIFIED (`"."`), which is the form * `/analytics/query` expects back in `measures[]` / `dimensions[]`; the @@ -101,12 +101,12 @@ export const GetAnalyticsMetaRequestSchema = lazySchema(() => z.object({ * `description`, `granularities` and `format` are dropped by the projection and * are NOT reachable through this endpoint (#6442). * - * No bare type alias by design — `CubeMeta` in `contracts/analytics-service.ts` - * is already THE name for this shape, and a second name for one type is the - * permanent synonym ADR-0122 D3 forbids. `analytics-meta-response.test.ts` pins - * the two against each other at compile time. + * Module-local, and NOT exported as its own named schema: `CubeMeta` in + * `contracts/analytics-service.ts` is already THE name for this shape, so a + * second exported name would be the permanent synonym ADR-0122 D3 forbids AND a + * new dual-source export. `analytics.test.ts` binds the two at compile time. */ -export const CubeMetaMemberSchema = lazySchema(() => z.object({ +const cubeMetaMemberShape = () => z.object({ name: z.string().describe('Cube-qualified member name, `"."` — the spelling `/analytics/query` accepts'), type: z.string().describe( 'Aggregation type for a measure (`AggregationMetricType`) or data type for a ' @@ -115,26 +115,7 @@ export const CubeMetaMemberSchema = lazySchema(() => z.object({ + 'shape serves both member kinds.', ), title: z.string().optional().describe('Display label, projected from the definition\'s `label`'), -})); - -/** - * One cube as `GET /analytics/meta` publishes it — the `CubeMeta` discovery - * projection declared in `contracts/analytics-service.ts` and produced - * identically by both implementations of `AnalyticsService.getMeta`. - * - * Carries only what a client needs to BUILD a query (which cubes exist, and - * which measures/dimensions each accepts). The cube's `sql`, `joins`, - * `refreshKey`, `public` and `description` are not projected — `CubeSchema` in - * `data/analytics.zod.ts` remains the authoring definition. - * - * No bare type alias — see the note on {@link CubeMetaMemberSchema}. - */ -export const CubeMetaSchema = lazySchema(() => z.object({ - name: z.string().describe('Cube name'), - title: z.string().optional().describe('Human-readable cube title'), - measures: z.array(CubeMetaMemberSchema).describe('Measures this cube accepts in `/analytics/query`'), - dimensions: z.array(CubeMetaMemberSchema).describe('Dimensions this cube accepts in `/analytics/query`'), -})); +}); /** * Meta Response @@ -164,9 +145,15 @@ export const CubeMetaSchema = lazySchema(() => z.object({ * consumer pulling it. */ export const AnalyticsMetadataResponseSchema = lazySchema(() => BaseResponseSchema.extend({ - data: z.array(CubeMetaSchema).describe( - 'Available cubes, each as the `CubeMeta` discovery projection. A bare array — ' - + 'there is no `cubes` wrapper object.', + data: z.array(z.object({ + name: z.string().describe('Cube name'), + title: z.string().optional().describe('Human-readable cube title'), + measures: z.array(cubeMetaMemberShape()).describe('Measures this cube accepts in `/analytics/query`'), + dimensions: z.array(cubeMetaMemberShape()).describe('Dimensions this cube accepts in `/analytics/query`'), + })).describe( + 'Available cubes, each as the `CubeMeta` discovery projection — the cube name, ' + + 'its title, and the measures/dimensions a client may name in a query. A bare ' + + 'array: there is no `cubes` wrapper object, and no cube `sql` is published.', ), })); From ec65e46b3d0724e36211d5c283841e22edb12c02 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 08:21:59 +0000 Subject: [PATCH 3/4] chore(spec): finish the post-merge wholesale regen on the merged tree Recovery commit: the dev agent was killed by a container restart after committing the merge of origin/main and before the regen chain finished; this completes the four-step (docs references, api-surface, ledger counts). check:generated 10/10. --- content/docs/references/api/analytics.mdx | 2 +- content/docs/references/api/protocol.mdx | 41 ++++++++++++++++++- content/docs/references/index.mdx | 10 ++--- ...07-unknown-key-strictness-ledger.counts.md | 2 +- packages/spec/api-surface/api.json | 2 + packages/spec/authorable-surface/api.json | 27 ++++++++++++ packages/spec/json-schema.manifest/api.json | 1 + .../src/type-alias-convention.pin.test.ts | 8 +++- 8 files changed, 83 insertions(+), 10 deletions(-) diff --git a/content/docs/references/api/analytics.mdx b/content/docs/references/api/analytics.mdx index ad22e17a09..35cf63713b 100644 --- a/content/docs/references/api/analytics.mdx +++ b/content/docs/references/api/analytics.mdx @@ -46,7 +46,7 @@ const result = AnalyticsEndpoint.parse(data); | **success** | `boolean` | ✅ | Operation success status | | **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +257 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | -| **data** | `{ cubes: object[] }` | ✅ | | +| **data** | `{ name: string; title?: string; measures: object[]; dimensions: object[] }[]` | ✅ | Available cubes, each as the `CubeMeta` discovery projection — the cube name, its title, and the measures/dimensions a client may name in a query. A bare array: there is no `cubes` wrapper object, and no cube `sql` is published. | --- diff --git a/content/docs/references/api/protocol.mdx b/content/docs/references/api/protocol.mdx index eecf05b587..4c6101e001 100644 --- a/content/docs/references/api/protocol.mdx +++ b/content/docs/references/api/protocol.mdx @@ -12,8 +12,8 @@ description: Protocol protocol schemas ## TypeScript Usage ```typescript -import { AiAgentCapabilitiesSchema, AiAgentChatRequestSchema, AiAgentSummarySchema, AiAgentsResponseSchema, AiChatRequestSchema, AiChatResponseSchema, AiCompleteRequestSchema, AiConversationSchema, AiMessageSchema, AiModelsResponseSchema, AiPendingActionSchema, AiPendingActionStatusSchema, AiStreamChunkSchema, ApproveAiPendingActionResponseSchema, AutomationActionsResponseSchema, AutomationTriggerRequestSchema, AutomationTriggerResponseSchema, BatchDataRequestSchema, BatchDataResponseSchema, CheckPermissionRequestSchema, CheckPermissionResponseSchema, CreateAiConversationRequestSchema, CreateDataRequestSchema, CreateDataResponseSchema, CreateManyDataRequestSchema, CreateManyDataResponseSchema, DeleteDataRequestSchema, DeleteDataResponseSchema, DeleteManyDataRequestSchema, DeleteManyDataResponseSchema, DeleteMetaItemRequestSchema, DeleteMetaItemResponseSchema, DisablePackageRequestSchema, DisablePackageResponseSchema, EnablePackageRequestSchema, EnablePackageResponseSchema, FindDataRequestSchema, FindDataResponseSchema, GetDataRequestSchema, GetDataResponseSchema, GetDiscoveryRequestSchema, GetDiscoveryResponseSchema, GetEffectivePermissionsRequestSchema, GetEffectivePermissionsResponseSchema, GetFieldLabelsRequestSchema, GetFieldLabelsResponseSchema, GetLocalesRequestSchema, GetLocalesResponseSchema, GetMetaItemCachedRequestSchema, GetMetaItemCachedResponseSchema, GetMetaItemRequestSchema, GetMetaItemResponseSchema, GetMetaItemsRequestSchema, GetMetaItemsResponseSchema, GetMetaTypesRequestSchema, GetMetaTypesResponseSchema, GetNotificationPreferencesRequestSchema, GetNotificationPreferencesResponseSchema, GetObjectPermissionsRequestSchema, GetObjectPermissionsResponseSchema, GetPackageRequestSchema, GetPackageResponseSchema, GetPresenceRequestSchema, GetPresenceResponseSchema, GetTranslationsRequestSchema, GetTranslationsResponseSchema, GetUiViewRequestSchema, GetUiViewResponseSchema, HttpFindQueryParamsSchema, InstallPackageRequestSchema, InstallPackageResponseSchema, ListAiConversationsRequestSchema, ListAiConversationsResponseSchema, ListAiPendingActionsRequestSchema, ListAiPendingActionsResponseSchema, ListNotificationsRequestSchema, ListNotificationsResponseSchema, ListPackagesRequestSchema, ListPackagesResponseSchema, MarkAllNotificationsReadRequestSchema, MarkAllNotificationsReadResponseSchema, MarkNotificationsReadRequestSchema, MarkNotificationsReadResponseSchema, NotificationSchema, NotificationPreferencesSchema, RealtimeConnectRequestSchema, RealtimeConnectResponseSchema, RealtimeDisconnectRequestSchema, RealtimeDisconnectResponseSchema, RealtimeSubscribeRequestSchema, RealtimeSubscribeResponseSchema, RealtimeUnsubscribeRequestSchema, RealtimeUnsubscribeResponseSchema, RegisterDeviceRequestSchema, RegisterDeviceResponseSchema, RejectAiPendingActionResponseSchema, SaveMetaItemRequestSchema, SaveMetaItemResponseSchema, SetPresenceRequestSchema, SetPresenceResponseSchema, UninstallPackageRequestSchema, UninstallPackageResponseSchema, UnregisterDeviceRequestSchema, UnregisterDeviceResponseSchema, UpdateAiConversationRequestSchema, UpdateDataRequestSchema, UpdateDataResponseSchema, UpdateManyDataRequestSchema, UpdateManyDataResponseSchema, UpdateNotificationPreferencesRequestSchema, UpdateNotificationPreferencesResponseSchema, ValidateDataIssueSchema, ValidateDataRequestSchema, ValidateDataResponseSchema } from '@objectstack/spec/api'; -import type { AiAgentCapabilities, AiAgentChatRequest, AiAgentSummary, AiAgentsResponse, AiChatRequest, AiChatResponse, AiCompleteRequest, AiConversation, AiMessage, AiModelsResponse, AiPendingAction, AiPendingActionStatus, AiStreamChunk, ApproveAiPendingActionResponse, AutomationActionsResponse, AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CheckPermissionRequest, CheckPermissionResponse, CreateAiConversationRequest, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, DeleteMetaItemRequest, DeleteMetaItemResponse, DisablePackageRequest, DisablePackageResponse, EnablePackageRequest, EnablePackageResponse, FindDataRequest, FindDataResponse, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetEffectivePermissionsRequest, GetEffectivePermissionsResponse, GetFieldLabelsRequest, GetFieldLabelsResponse, GetLocalesRequest, GetLocalesResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetObjectPermissionsRequest, GetObjectPermissionsResponse, GetPackageRequest, GetPackageResponse, GetPresenceRequest, GetPresenceResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetUiViewResponse, InstallPackageRequest, InstallPackageResponse, ListAiConversationsRequest, ListAiConversationsResponse, ListAiPendingActionsRequest, ListAiPendingActionsResponse, ListNotificationsRequest, ListNotificationsResponse, ListPackagesRequest, ListPackagesResponse, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, Notification, NotificationPreferences, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, RejectAiPendingActionResponse, SaveMetaItemRequest, SaveMetaItemResponse, SetPresenceRequest, SetPresenceResponse, UninstallPackageRequest, UninstallPackageResponse, UnregisterDeviceRequest, UnregisterDeviceResponse, UpdateAiConversationRequest, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse, UpdateNotificationPreferencesRequest, UpdateNotificationPreferencesResponse, ValidateDataIssue, ValidateDataRequest, ValidateDataResponse } from '@objectstack/spec/api'; +import { AiAgentCapabilitiesSchema, AiAgentChatRequestSchema, AiAgentSummarySchema, AiAgentsResponseSchema, AiChatRequestSchema, AiChatResponseSchema, AiCompleteRequestSchema, AiConversationSchema, AiMessageSchema, AiModelsResponseSchema, AiPendingActionSchema, AiPendingActionStatusSchema, AiStreamChunkSchema, ApproveAiPendingActionResponseSchema, AutomationActionsResponseSchema, AutomationTriggerRequestSchema, AutomationTriggerResponseSchema, BatchDataRequestSchema, BatchDataResponseSchema, CheckPermissionRequestSchema, CheckPermissionResponseSchema, CreateAiConversationRequestSchema, CreateDataRequestSchema, CreateDataResponseSchema, CreateManyDataRequestSchema, CreateManyDataResponseSchema, DeleteDataRequestSchema, DeleteDataResponseSchema, DeleteManyDataRequestSchema, DeleteManyDataResponseSchema, DeleteMetaItemRequestSchema, DeleteMetaItemResponseSchema, DisablePackageRequestSchema, DisablePackageResponseSchema, EnablePackageRequestSchema, EnablePackageResponseSchema, FindDataRequestSchema, FindDataResponseSchema, GetDataRequestSchema, GetDataResponseSchema, GetDiscoveryRequestSchema, GetDiscoveryResponseSchema, GetEffectivePermissionsRequestSchema, GetEffectivePermissionsResponseSchema, GetFieldLabelsRequestSchema, GetFieldLabelsResponseSchema, GetLocalesRequestSchema, GetLocalesResponseSchema, GetMetaItemCachedRequestSchema, GetMetaItemCachedResponseSchema, GetMetaItemLayeredResponseSchema, GetMetaItemRequestSchema, GetMetaItemResponseSchema, GetMetaItemsRequestSchema, GetMetaItemsResponseSchema, GetMetaTypesRequestSchema, GetMetaTypesResponseSchema, GetNotificationPreferencesRequestSchema, GetNotificationPreferencesResponseSchema, GetObjectPermissionsRequestSchema, GetObjectPermissionsResponseSchema, GetPackageRequestSchema, GetPackageResponseSchema, GetPresenceRequestSchema, GetPresenceResponseSchema, GetTranslationsRequestSchema, GetTranslationsResponseSchema, GetUiViewRequestSchema, GetUiViewResponseSchema, HttpFindQueryParamsSchema, InstallPackageRequestSchema, InstallPackageResponseSchema, ListAiConversationsRequestSchema, ListAiConversationsResponseSchema, ListAiPendingActionsRequestSchema, ListAiPendingActionsResponseSchema, ListNotificationsRequestSchema, ListNotificationsResponseSchema, ListPackagesRequestSchema, ListPackagesResponseSchema, MarkAllNotificationsReadRequestSchema, MarkAllNotificationsReadResponseSchema, MarkNotificationsReadRequestSchema, MarkNotificationsReadResponseSchema, NotificationSchema, NotificationPreferencesSchema, RealtimeConnectRequestSchema, RealtimeConnectResponseSchema, RealtimeDisconnectRequestSchema, RealtimeDisconnectResponseSchema, RealtimeSubscribeRequestSchema, RealtimeSubscribeResponseSchema, RealtimeUnsubscribeRequestSchema, RealtimeUnsubscribeResponseSchema, RegisterDeviceRequestSchema, RegisterDeviceResponseSchema, RejectAiPendingActionResponseSchema, SaveMetaItemRequestSchema, SaveMetaItemResponseSchema, SetPresenceRequestSchema, SetPresenceResponseSchema, UninstallPackageRequestSchema, UninstallPackageResponseSchema, UnregisterDeviceRequestSchema, UnregisterDeviceResponseSchema, UpdateAiConversationRequestSchema, UpdateDataRequestSchema, UpdateDataResponseSchema, UpdateManyDataRequestSchema, UpdateManyDataResponseSchema, UpdateNotificationPreferencesRequestSchema, UpdateNotificationPreferencesResponseSchema, ValidateDataIssueSchema, ValidateDataRequestSchema, ValidateDataResponseSchema } from '@objectstack/spec/api'; +import type { AiAgentCapabilities, AiAgentChatRequest, AiAgentSummary, AiAgentsResponse, AiChatRequest, AiChatResponse, AiCompleteRequest, AiConversation, AiMessage, AiModelsResponse, AiPendingAction, AiPendingActionStatus, AiStreamChunk, ApproveAiPendingActionResponse, AutomationActionsResponse, AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CheckPermissionRequest, CheckPermissionResponse, CreateAiConversationRequest, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, DeleteMetaItemRequest, DeleteMetaItemResponse, DisablePackageRequest, DisablePackageResponse, EnablePackageRequest, EnablePackageResponse, FindDataRequest, FindDataResponse, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetEffectivePermissionsRequest, GetEffectivePermissionsResponse, GetFieldLabelsRequest, GetFieldLabelsResponse, GetLocalesRequest, GetLocalesResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemLayeredResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetObjectPermissionsRequest, GetObjectPermissionsResponse, GetPackageRequest, GetPackageResponse, GetPresenceRequest, GetPresenceResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetUiViewResponse, InstallPackageRequest, InstallPackageResponse, ListAiConversationsRequest, ListAiConversationsResponse, ListAiPendingActionsRequest, ListAiPendingActionsResponse, ListNotificationsRequest, ListNotificationsResponse, ListPackagesRequest, ListPackagesResponse, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, Notification, NotificationPreferences, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, RejectAiPendingActionResponse, SaveMetaItemRequest, SaveMetaItemResponse, SetPresenceRequest, SetPresenceResponse, UninstallPackageRequest, UninstallPackageResponse, UnregisterDeviceRequest, UnregisterDeviceResponse, UpdateAiConversationRequest, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse, UpdateNotificationPreferencesRequest, UpdateNotificationPreferencesResponse, ValidateDataIssue, ValidateDataRequest, ValidateDataResponse } from '@objectstack/spec/api'; // Validate data const result = AiAgentCapabilitiesSchema.parse(data); @@ -699,6 +699,33 @@ Enable package response | **version** | `string` | optional | Metadata version identifier | +--- + +## GetMetaItemLayeredResponse + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `string` | ✅ | Metadata type name (canonical singular) | +| **name** | `string` | ✅ | Item name | +| **code** | `any` | ✅ | LAYER 1 — the packaged artifact baseline exactly as shipped, before any tenant customization. `null` when no artifact ships this item (it exists only as an overlay). | +| **overlay** | `any` | ✅ | LAYER 2 — the stored customization row ALONE, not merged with `code`. `null` when this tenant has not customized the item. | +| **overlayScope** | `Enum<'org' \| 'env'> \| null` | ✅ | Which scope the `overlay` row was read from — `org` for a tenant overlay, `env` for an environment-level one. `null` exactly when `overlay` is null. | +| **effective** | `any` | ✅ | LAYER 3 — the merged result, i.e. the value an ordinary `GET /meta/:type/:name` would return under `item`. `null` when the item resolves to nothing at all. | +| **_diagnostics** | `{ valid: boolean; errors?: object[]; warnings?: object[] }` | optional | Load-time spec-validation verdict for `effective`, so the Studio edit page can raise invalid-metadata banners and inline field errors without a second round trip. ABSENT for metadata types that register no Zod schema (function / service / router) — absence means "no opinion", never "valid". | +| **lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | ✅ | Resolved lock verdict (ADR-0010 §3.3), artifact winning over overlay. Always present on this path. | +| **lockReason** | `string` | optional | Human-readable explanation shown next to a refused write. Present only when the resolved item declares `_lockReason`. | +| **lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Which layer asserted the lock. Present only when the resolved item declares `_lockSource`. | +| **lockDocsUrl** | `string` | optional | Documentation link surfaced beside `lockReason`. Present only when the resolved item declares `_lockDocsUrl`. | +| **provenance** | `Enum<'package' \| 'org' \| 'env-forced'>` | optional | Where the item came from (package \| org \| env-forced). Present only when the resolved item declares `_provenance`. | +| **packageId** | `string` | optional | Owning package machine id. Present only when the resolved item declares `_packageId`. | +| **packageVersion** | `string` | optional | Owning package version. Present only when the resolved item declares `_packageVersion`. | +| **editable** | `boolean` | ✅ | Whether an overlay write is permitted. Always present on this path. | +| **deletable** | `boolean` | ✅ | Whether deleting the overlay is permitted. Always present on this path. | +| **resettable** | `boolean` | ✅ | Whether the item can be reset to its packaged default. Always present on this path. | + + --- ## GetMetaItemRequest @@ -723,6 +750,16 @@ Enable package response | **type** | `string` | ✅ | Metadata type name | | **name** | `string` | ✅ | Item name | | **item** | `any` | ✅ | Metadata item definition | +| **lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Resolved lock verdict for this item (ADR-0010 §3.3). `none` means unlocked; `no-overlay` / `no-delete` / `full` refuse the corresponding write with 403 `ITEM_LOCKED`. Resolved from the document's `_lock`, with the packaged artifact winning over any org overlay. | +| **lockReason** | `string` | optional | Human-readable explanation shown next to a refused write. Present only when the resolved item declares `_lockReason`. | +| **lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Which layer asserted the lock. Present only when the resolved item declares `_lockSource`. | +| **lockDocsUrl** | `string` | optional | Documentation link surfaced beside `lockReason`. Present only when the resolved item declares `_lockDocsUrl`. | +| **provenance** | `Enum<'package' \| 'org' \| 'env-forced'>` | optional | Where the item came from (package \| org \| env-forced). Present only when the resolved item declares `_provenance`. | +| **packageId** | `string` | optional | Owning package machine id. Present only when the resolved item declares `_packageId`. | +| **packageVersion** | `string` | optional | Owning package version. Present only when the resolved item declares `_packageVersion`. | +| **editable** | `boolean` | optional | Whether an overlay write is permitted — false iff `lock` is `no-overlay` or `full`. A derived verdict: do not recompute it from `lock` client-side. | +| **deletable** | `boolean` | optional | Whether deleting the overlay is permitted — false iff `lock` is `no-delete` or `full`. | +| **resettable** | `boolean` | optional | Whether the item can be reset to its packaged default — true iff it is artifact-backed, i.e. there is a baseline to reset TO. | --- diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx index 94114bf1b9..5faafb095f 100644 --- a/content/docs/references/index.mdx +++ b/content/docs/references/index.mdx @@ -1,6 +1,6 @@ --- title: Protocol Reference -description: Every schema published by @objectstack/spec — 1583 schemas across 14 protocol modules +description: Every schema published by @objectstack/spec — 1584 schemas across 14 protocol modules --- {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} @@ -20,7 +20,7 @@ counts are sums of the rows they head. Regenerate with | Module | Pages | Schemas | Description | | :--- | ---: | ---: | :--- | | [AI Protocol](/docs/references/ai) | 11 | 66 | Agents, tools, skills, RAG and knowledge sources, model registry, conversations. | -| [API Protocol](/docs/references/api) | 28 | 409 | REST/GraphQL contracts, endpoints, routing, realtime, batch, discovery. | +| [API Protocol](/docs/references/api) | 28 | 410 | REST/GraphQL contracts, endpoints, routing, realtime, batch, discovery. | | [Automation Protocol](/docs/references/automation) | 13 | 68 | Flows and their nodes, approvals, ETL pipelines, webhooks, state machines, execution records. | | [Cloud Protocol](/docs/references/cloud) | 11 | 94 | Environments, packages and versions, marketplace, developer portal, tenancy. | | [Data Protocol](/docs/references/data) | 29 | 164 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. | @@ -33,7 +33,7 @@ counts are sums of the rows they head. Regenerate with | [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. | | [System Protocol](/docs/references/system) | 37 | 292 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. | | [UI Protocol](/docs/references/ui) | 16 | 147 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | -| **Total** | **199** | **1583** | 14 protocol modules | +| **Total** | **199** | **1584** | 14 protocol modules | --- @@ -61,7 +61,7 @@ Agents, tools, skills, RAG and knowledge sources, model registry, conversations. ## API Protocol -**Source:** `packages/spec/src/api/` · **Import:** `@objectstack/spec/api` · **28 pages, 409 schemas** +**Source:** `packages/spec/src/api/` · **Import:** `@objectstack/spec/api` · **28 pages, 410 schemas** REST/GraphQL contracts, endpoints, routing, realtime, batch, discovery. @@ -86,7 +86,7 @@ REST/GraphQL contracts, endpoints, routing, realtime, batch, discovery. | [`odata.zod.ts`](/docs/references/api/odata) | `ODataConfig`, `ODataError`, `ODataFilterFunction`, `ODataMetadata`, `ODataQuery`, `ODataResponse` | | [`package-api.zod.ts`](/docs/references/api/package-api) | `GetInstalledPackageRequest`, `GetInstalledPackageResponse`, `ListInstalledPackagesRequest`, `ListInstalledPackagesResponse`, `PackageApiErrorCode`, `PackageInstallRequest`, `PackageInstallResponse`, `PackagePathParams`, `PackageRollbackRequest`, `PackageRollbackResponse`, `PackageUpgradeRequest`, `PackageUpgradeResponse`, `ResolveDependenciesRequest`, `ResolveDependenciesResponse`, `UninstallPackageApiRequest`, `UninstallPackageApiResponse`, `UploadArtifactRequest`, `UploadArtifactResponse` | | [`plugin-rest-api.zod.ts`](/docs/references/api/plugin-rest-api) | `ErrorHandlingConfig`, `HandlerStatus`, `OpenApiGenerationConfig`, `RequestValidationConfig`, `ResponseEnvelopeConfig`, `RestApiEndpoint`, `RestApiPluginConfig`, `RestApiRouteCategory`, `RestApiRouteRegistration`, `RouteCoverageEntry`, `RouteCoverageReport`, `ValidationMode` | -| [`protocol.zod.ts`](/docs/references/api/protocol) | `AiAgentCapabilities`, `AiAgentChatRequest`, `AiAgentSummary`, `AiAgentsResponse`, `AiChatRequest`, `AiChatResponse`, `AiCompleteRequest`, `AiConversation`, `AiMessage`, `AiModelsResponse`, `AiPendingAction`, `AiPendingActionStatus`, `AiStreamChunk`, `ApproveAiPendingActionResponse`, `AutomationActionsResponse`, `AutomationTriggerRequest`, `AutomationTriggerResponse`, `BatchDataRequest`, `BatchDataResponse`, `CheckPermissionRequest`, `CheckPermissionResponse`, `CreateAiConversationRequest`, `CreateDataRequest`, `CreateDataResponse`, `CreateManyDataRequest`, `CreateManyDataResponse`, `DeleteDataRequest`, `DeleteDataResponse`, `DeleteManyDataRequest`, `DeleteManyDataResponse`, `DeleteMetaItemRequest`, `DeleteMetaItemResponse`, `DisablePackageRequest`, `DisablePackageResponse`, `EnablePackageRequest`, `EnablePackageResponse`, `FindDataRequest`, `FindDataResponse`, `GetDataRequest`, `GetDataResponse`, `GetDiscoveryRequest`, `GetDiscoveryResponse`, `GetEffectivePermissionsRequest`, `GetEffectivePermissionsResponse`, `GetFieldLabelsRequest`, `GetFieldLabelsResponse`, `GetLocalesRequest`, `GetLocalesResponse`, `GetMetaItemCachedRequest`, `GetMetaItemCachedResponse`, `GetMetaItemRequest`, `GetMetaItemResponse`, `GetMetaItemsRequest`, `GetMetaItemsResponse`, `GetMetaTypesRequest`, `GetMetaTypesResponse`, `GetNotificationPreferencesRequest`, `GetNotificationPreferencesResponse`, `GetObjectPermissionsRequest`, `GetObjectPermissionsResponse`, `GetPackageRequest`, `GetPackageResponse`, `GetPresenceRequest`, `GetPresenceResponse`, `GetTranslationsRequest`, `GetTranslationsResponse`, `GetUiViewRequest`, `GetUiViewResponse`, `HttpFindQueryParams`, `InstallPackageRequest`, `InstallPackageResponse`, `ListAiConversationsRequest`, `ListAiConversationsResponse`, `ListAiPendingActionsRequest`, `ListAiPendingActionsResponse`, `ListNotificationsRequest`, `ListNotificationsResponse`, `ListPackagesRequest`, `ListPackagesResponse`, `MarkAllNotificationsReadRequest`, `MarkAllNotificationsReadResponse`, `MarkNotificationsReadRequest`, `MarkNotificationsReadResponse`, `Notification`, `NotificationPreferences`, `RealtimeConnectRequest`, `RealtimeConnectResponse`, `RealtimeDisconnectRequest`, `RealtimeDisconnectResponse`, `RealtimeSubscribeRequest`, `RealtimeSubscribeResponse`, `RealtimeUnsubscribeRequest`, `RealtimeUnsubscribeResponse`, `RegisterDeviceRequest`, `RegisterDeviceResponse`, `RejectAiPendingActionResponse`, `SaveMetaItemRequest`, `SaveMetaItemResponse`, `SetPresenceRequest`, `SetPresenceResponse`, `UninstallPackageRequest`, `UninstallPackageResponse`, `UnregisterDeviceRequest`, `UnregisterDeviceResponse`, `UpdateAiConversationRequest`, `UpdateDataRequest`, `UpdateDataResponse`, `UpdateManyDataRequest`, `UpdateManyDataResponse`, `UpdateNotificationPreferencesRequest`, `UpdateNotificationPreferencesResponse`, `ValidateDataIssue`, `ValidateDataRequest`, `ValidateDataResponse` | +| [`protocol.zod.ts`](/docs/references/api/protocol) | `AiAgentCapabilities`, `AiAgentChatRequest`, `AiAgentSummary`, `AiAgentsResponse`, `AiChatRequest`, `AiChatResponse`, `AiCompleteRequest`, `AiConversation`, `AiMessage`, `AiModelsResponse`, `AiPendingAction`, `AiPendingActionStatus`, `AiStreamChunk`, `ApproveAiPendingActionResponse`, `AutomationActionsResponse`, `AutomationTriggerRequest`, `AutomationTriggerResponse`, `BatchDataRequest`, `BatchDataResponse`, `CheckPermissionRequest`, `CheckPermissionResponse`, `CreateAiConversationRequest`, `CreateDataRequest`, `CreateDataResponse`, `CreateManyDataRequest`, `CreateManyDataResponse`, `DeleteDataRequest`, `DeleteDataResponse`, `DeleteManyDataRequest`, `DeleteManyDataResponse`, `DeleteMetaItemRequest`, `DeleteMetaItemResponse`, `DisablePackageRequest`, `DisablePackageResponse`, `EnablePackageRequest`, `EnablePackageResponse`, `FindDataRequest`, `FindDataResponse`, `GetDataRequest`, `GetDataResponse`, `GetDiscoveryRequest`, `GetDiscoveryResponse`, `GetEffectivePermissionsRequest`, `GetEffectivePermissionsResponse`, `GetFieldLabelsRequest`, `GetFieldLabelsResponse`, `GetLocalesRequest`, `GetLocalesResponse`, `GetMetaItemCachedRequest`, `GetMetaItemCachedResponse`, `GetMetaItemLayeredResponse`, `GetMetaItemRequest`, `GetMetaItemResponse`, `GetMetaItemsRequest`, `GetMetaItemsResponse`, `GetMetaTypesRequest`, `GetMetaTypesResponse`, `GetNotificationPreferencesRequest`, `GetNotificationPreferencesResponse`, `GetObjectPermissionsRequest`, `GetObjectPermissionsResponse`, `GetPackageRequest`, `GetPackageResponse`, `GetPresenceRequest`, `GetPresenceResponse`, `GetTranslationsRequest`, `GetTranslationsResponse`, `GetUiViewRequest`, `GetUiViewResponse`, `HttpFindQueryParams`, `InstallPackageRequest`, `InstallPackageResponse`, `ListAiConversationsRequest`, `ListAiConversationsResponse`, `ListAiPendingActionsRequest`, `ListAiPendingActionsResponse`, `ListNotificationsRequest`, `ListNotificationsResponse`, `ListPackagesRequest`, `ListPackagesResponse`, `MarkAllNotificationsReadRequest`, `MarkAllNotificationsReadResponse`, `MarkNotificationsReadRequest`, `MarkNotificationsReadResponse`, `Notification`, `NotificationPreferences`, `RealtimeConnectRequest`, `RealtimeConnectResponse`, `RealtimeDisconnectRequest`, `RealtimeDisconnectResponse`, `RealtimeSubscribeRequest`, `RealtimeSubscribeResponse`, `RealtimeUnsubscribeRequest`, `RealtimeUnsubscribeResponse`, `RegisterDeviceRequest`, `RegisterDeviceResponse`, `RejectAiPendingActionResponse`, `SaveMetaItemRequest`, `SaveMetaItemResponse`, `SetPresenceRequest`, `SetPresenceResponse`, `UninstallPackageRequest`, `UninstallPackageResponse`, `UnregisterDeviceRequest`, `UnregisterDeviceResponse`, `UpdateAiConversationRequest`, `UpdateDataRequest`, `UpdateDataResponse`, `UpdateManyDataRequest`, `UpdateManyDataResponse`, `UpdateNotificationPreferencesRequest`, `UpdateNotificationPreferencesResponse`, `ValidateDataIssue`, `ValidateDataRequest`, `ValidateDataResponse` | | [`query-adapter.zod.ts`](/docs/references/api/query-adapter) | `ODataQueryAdapter`, `OperatorMapping`, `QueryAdapterConfig`, `QueryAdapterTarget`, `RestQueryAdapter` | | [`realtime.zod.ts`](/docs/references/api/realtime) | `RealtimeConfig`, `RealtimeEvent`, `RealtimeEventType`, `RealtimePresence`, `Subscription`, `SubscriptionEvent`, `TransportProtocol` | | [`realtime-shared.zod.ts`](/docs/references/api/realtime-shared) | `BasePresence`, `PresenceStatus`, `RealtimeRecordAction` | diff --git a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md index 09d461a238..81aaa6d79c 100644 --- a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md +++ b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md @@ -264,7 +264,7 @@ directory rather than per file. | Dir | Sites | |---|---| | `ai/` | 77 | -| `api/` | 391 | +| `api/` | 393 | | `cloud/` | 83 | | `identity/` | 33 | | `integration/` | 10 | diff --git a/packages/spec/api-surface/api.json b/packages/spec/api-surface/api.json index ee12fb522e..a9b23a0648 100644 --- a/packages/spec/api-surface/api.json +++ b/packages/spec/api-surface/api.json @@ -412,6 +412,8 @@ "GetMetaItemCachedResponse (type)", "GetMetaItemCachedResponseParsed (type)", "GetMetaItemCachedResponseSchema (const)", + "GetMetaItemLayeredResponse (type)", + "GetMetaItemLayeredResponseSchema (const)", "GetMetaItemRequest (type)", "GetMetaItemRequestSchema (const)", "GetMetaItemResponse (type)", diff --git a/packages/spec/authorable-surface/api.json b/packages/spec/authorable-surface/api.json index 0d5c357a9e..9f4087270b 100644 --- a/packages/spec/authorable-surface/api.json +++ b/packages/spec/authorable-surface/api.json @@ -711,11 +711,38 @@ "api/GetMetaItemCachedResponse:lastModified", "api/GetMetaItemCachedResponse:notModified", "api/GetMetaItemCachedResponse:version", + "api/GetMetaItemLayeredResponse:_diagnostics", + "api/GetMetaItemLayeredResponse:code", + "api/GetMetaItemLayeredResponse:deletable", + "api/GetMetaItemLayeredResponse:editable", + "api/GetMetaItemLayeredResponse:effective", + "api/GetMetaItemLayeredResponse:lock", + "api/GetMetaItemLayeredResponse:lockDocsUrl", + "api/GetMetaItemLayeredResponse:lockReason", + "api/GetMetaItemLayeredResponse:lockSource", + "api/GetMetaItemLayeredResponse:name", + "api/GetMetaItemLayeredResponse:overlay", + "api/GetMetaItemLayeredResponse:overlayScope", + "api/GetMetaItemLayeredResponse:packageId", + "api/GetMetaItemLayeredResponse:packageVersion", + "api/GetMetaItemLayeredResponse:provenance", + "api/GetMetaItemLayeredResponse:resettable", + "api/GetMetaItemLayeredResponse:type", "api/GetMetaItemRequest:name", "api/GetMetaItemRequest:packageId", "api/GetMetaItemRequest:type", + "api/GetMetaItemResponse:deletable", + "api/GetMetaItemResponse:editable", "api/GetMetaItemResponse:item", + "api/GetMetaItemResponse:lock", + "api/GetMetaItemResponse:lockDocsUrl", + "api/GetMetaItemResponse:lockReason", + "api/GetMetaItemResponse:lockSource", "api/GetMetaItemResponse:name", + "api/GetMetaItemResponse:packageId", + "api/GetMetaItemResponse:packageVersion", + "api/GetMetaItemResponse:provenance", + "api/GetMetaItemResponse:resettable", "api/GetMetaItemResponse:type", "api/GetMetaItemsRequest:packageId", "api/GetMetaItemsRequest:type", diff --git a/packages/spec/json-schema.manifest/api.json b/packages/spec/json-schema.manifest/api.json index 0dda14515f..83e3e373ee 100644 --- a/packages/spec/json-schema.manifest/api.json +++ b/packages/spec/json-schema.manifest/api.json @@ -174,6 +174,7 @@ "api/GetLocalesResponse", "api/GetMetaItemCachedRequest", "api/GetMetaItemCachedResponse", + "api/GetMetaItemLayeredResponse", "api/GetMetaItemRequest", "api/GetMetaItemResponse", "api/GetMetaItemsRequest", diff --git a/packages/spec/src/type-alias-convention.pin.test.ts b/packages/spec/src/type-alias-convention.pin.test.ts index 4595b31a0b..712598f93a 100644 --- a/packages/spec/src/type-alias-convention.pin.test.ts +++ b/packages/spec/src/type-alias-convention.pin.test.ts @@ -448,7 +448,7 @@ export type Iso131 = Assert, export type Iso132 = Assert, z.infer< typeof M28.GetMetaItemsResponseSchema > >>; export type Iso133 = Assert, z.infer< typeof M28.GetMetaItemRequestSchema > >>; export type Iso134 = Assert, z.infer< typeof M28.GetMetaItemResponseSchema > >>; -export type Iso759 = Assert, z.infer< typeof M28.GetMetaItemLayeredResponseSchema > >>; +export type Iso760 = Assert, z.infer< typeof M28.GetMetaItemLayeredResponseSchema > >>; export type Iso135 = Assert, z.infer< typeof M28.SaveMetaItemRequestSchema > >>; export type Iso136 = Assert, z.infer< typeof M28.SaveMetaItemResponseSchema > >>; export type Iso137 = Assert, z.infer< typeof M28.DeleteMetaItemRequestSchema > >>; @@ -1566,6 +1566,12 @@ describe('ADR-0122 type-alias convention', () => { // either side's 749 verbatim would have been green in review and wrong in // fact, which is the exact failure this receipt block exists to prevent. // Recomputed from the merged file: 748 + 1 + 1 = 750. + // + // The collision was not only in the COUNT: both branches also minted the + // same next-free alias name, `Iso759`, so the merged file declared it + // twice. tsc caught that as TS2300 where the count assertion could not — + // `toHaveLength` counts matching LINES, and two lines that share a name + // still count as two. This sweep's pin took `Iso760`; #5728's kept 759. const self = readFileSync(fileURLToPath(import.meta.url), 'utf8'); const pins = self.match(/^export type Iso\d+ = Assert Date: Sat, 8 Aug 2026 08:49:28 +0000 Subject: [PATCH 4/4] chore(spec): regenerate generated surfaces from the merged tree (post ADR-0106 + #4593 backfill merge) --- content/docs/references/api/analytics.mdx | 2 +- packages/spec/api-surface/api.json | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/content/docs/references/api/analytics.mdx b/content/docs/references/api/analytics.mdx index 35cf63713b..b5a798132c 100644 --- a/content/docs/references/api/analytics.mdx +++ b/content/docs/references/api/analytics.mdx @@ -18,7 +18,7 @@ Provides endpoints for executing analytical queries and discovering metadata. ```typescript import { AnalyticsEndpoint, AnalyticsMetadataResponseSchema, AnalyticsQueryRequestSchema, AnalyticsResultResponseSchema, AnalyticsSqlResponseSchema, GetAnalyticsMetaRequestSchema } from '@objectstack/spec/api'; -import type { AnalyticsEndpoint, AnalyticsMetadataResponse, AnalyticsQueryRequest, AnalyticsSqlResponse } from '@objectstack/spec/api'; +import type { AnalyticsEndpoint, AnalyticsMetadataResponse, AnalyticsQueryRequest, AnalyticsSqlResponse, GetAnalyticsMetaRequest } from '@objectstack/spec/api'; // Validate data const result = AnalyticsEndpoint.parse(data); diff --git a/packages/spec/api-surface/api.json b/packages/spec/api-surface/api.json index a9b23a0648..54d92315d5 100644 --- a/packages/spec/api-surface/api.json +++ b/packages/spec/api-surface/api.json @@ -53,6 +53,7 @@ "ApiError (type)", "ApiErrorParsed (type)", "ApiErrorSchema (const)", + "ApiMapping (type)", "ApiMappingSchema (const)", "ApiRoutes (type)", "ApiRoutesSchema (const)", @@ -367,6 +368,7 @@ "GeneratedApiDocumentationSchema (const)", "GeneratedEndpoint (type)", "GeneratedEndpointSchema (const)", + "GetAnalyticsMetaRequest (type)", "GetAnalyticsMetaRequestSchema (const)", "GetAuthConfigResponse (type)", "GetAuthConfigResponseParsed (type)", @@ -575,6 +577,7 @@ "MetadataBulkResponse (type)", "MetadataBulkResponseParsed (type)", "MetadataBulkResponseSchema (const)", + "MetadataBulkUnregisterRequest (type)", "MetadataBulkUnregisterRequestSchema (const)", "MetadataCacheApi (const)", "MetadataCacheRequest (type)", @@ -638,6 +641,7 @@ "MetadataTypesResponse (type)", "MetadataTypesResponseParsed (type)", "MetadataTypesResponseSchema (const)", + "MetadataValidateRequest (type)", "MetadataValidateRequestSchema (const)", "MetadataValidateResponse (type)", "MetadataValidateResponseParsed (type)", @@ -869,6 +873,7 @@ "SubscribeMessage (type)", "SubscribeMessageSchema (const)", "Subscription (type)", + "SubscriptionEvent (type)", "SubscriptionEventSchema (const)", "SubscriptionSchema (const)", "ToggleFlowRequest (type)",