From 0baa300a9850c30fcaa7ba7e815c691df26e1598 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Oct 2026 18:20:54 +0000 Subject: [PATCH 1/5] fix(layout,types): an app-schema-renderer node draws the app document it carries under `schema` The registration published a `schema` input no node could deliver: SchemaRenderer strips a node's `schema` key and hands the registered component the node itself, so AppSchemaRenderer took the node for its app document. A document nested under `schema` drew an empty shell, and only undeclared app keys written flat on the node drew. Under triage ruling A, `registerLayout()` now registers the key against an adapter (the DetailSectionNode shape) that hands `node.schema` to AppSchemaRenderer, and AppSchemaRendererNodeSchema declares `schema` as AppComponentSchema by reference, optional. The document keeps its own refusals (mobileNavMode inside it), the flat spelling is refused by the strict face and no longer read, and basePath / mobileNavMode are unchanged. The layout guide says what the node does, its `children` sentence included. Claude-Session: https://claude.ai/code/session_01YLg8XqWGJ785fwQ5v4pH37 Co-authored-by: Claude --- ...1494-layout-app-schema-renderer-adapter.md | 13 ++ .../11494-types-app-schema-renderer-schema.md | 16 +++ content/docs/guide/layout.md | 40 +++++- ...-schema-renderer-arm-inputs-11440.test.tsx | 39 ++---- ...chema-renderer-schema-input-11494.test.tsx | 106 ++++++++++++++++ packages/layout/src/index.ts | 54 +++++++- .../app-mobile-nav-mode-refusal-11363.test.ts | 10 +- ...schema-renderer-schema-input-11494.test.ts | 115 ++++++++++++++++++ .../__tests__/passing-keys-arms-11440.test.ts | 13 +- .../src/__tests__/zod-mirror-parity.test.ts | 6 +- packages/types/src/zod/README.md | 2 +- packages/types/src/zod/app.zod.ts | 39 ++++-- 12 files changed, 398 insertions(+), 55 deletions(-) create mode 100644 .changeset/11494-layout-app-schema-renderer-adapter.md create mode 100644 .changeset/11494-types-app-schema-renderer-schema.md create mode 100644 packages/layout/src/__tests__/app-schema-renderer-schema-input-11494.test.tsx create mode 100644 packages/types/src/__tests__/app-schema-renderer-schema-input-11494.test.ts diff --git a/.changeset/11494-layout-app-schema-renderer-adapter.md b/.changeset/11494-layout-app-schema-renderer-adapter.md new file mode 100644 index 0000000000..db82e9eb97 --- /dev/null +++ b/.changeset/11494-layout-app-schema-renderer-adapter.md @@ -0,0 +1,13 @@ +--- +'@object-ui/layout': minor +--- + +An `app-schema-renderer` node draws the app document it carries under `schema` (objectui#11494, triage ruling A). + +**Breaking for a node that writes the app document's keys flat on itself** (shipped as `minor`, per this repository's version policy). `SchemaRenderer` hands every registered component the node itself as its `schema` prop, and `registerLayout()` registered `AppSchemaRenderer` directly, so the component took the node for its app document: a document nested under the node's `schema` key drew an empty shell, and only app keys written flat on the node (`navigation`, `title`, …), which no schema declares, drew. `registerLayout()` now registers the key against an adapter that hands `node.schema` to `AppSchemaRenderer`: + +- `{ "type": "app-schema-renderer", "schema": { "type": "app", "name": "crm", "navigation": [ … ] } }` draws the document's branding and navigation. +- The same keys written flat on the node are no longer read. Move them under `schema`, with `"type": "app"`. No producer in objectui or objectstack authors them flat. +- A node without `schema` draws the shell with no branding and no navigation, as it did before. + +Unchanged: `basePath` and `mobileNavMode` are read off the node, the registration's three declared `inputs`, and `AppSchemaRenderer` itself, whose JSX hosts pass `schema={appDocument}` directly. A node's `children` are still not rendered. diff --git a/.changeset/11494-types-app-schema-renderer-schema.md b/.changeset/11494-types-app-schema-renderer-schema.md new file mode 100644 index 0000000000..b0fdef23a6 --- /dev/null +++ b/.changeset/11494-types-app-schema-renderer-schema.md @@ -0,0 +1,16 @@ +--- +'@object-ui/types': minor +--- + +`AppSchemaRendererNodeSchema` declares the `app-schema-renderer` node's `schema` input: it is the app document, `AppComponentSchema` itself, by reference, and optional (objectui#11494, triage ruling A). objectui#11440 left `schema` undeclared because no node delivered it; `@object-ui/layout` now delivers it, so that sentence of objectui#11440's entry no longer holds. + +**Clause-②: yes** — the node's accept set changes on both faces: + +- **Strict face (`StrictAnyComponentSchema`): widens.** A node carrying its app document under `schema` validates; it was refused there with `unrecognized_keys` naming `schema`. The nested document is closed like every other object on this face, so an unknown key inside it is refused by name. +- **Tolerant face (`safeValidateSchema`, which `objectui validate` runs): narrows for a malformed document.** `schema` used to pass unjudged, as every undeclared key does; it is now judged as the app document. A nested value that is not one is refused at its own path: a document without `"type": "app"` at `schema.type` (`invalid_value`), a non-object at `schema` (`invalid_type`), and `mobileNavMode` inside the document at `schema.mobileNavMode`, by the app document's own objectui#11363 refusal. No producer in objectui or objectstack authors an `app-schema-renderer` node with a nested document, so nothing in either repository is refused. + +What stays the same: `basePath` and `mobileNavMode` on the node, as declared since objectui#11440 (a node without `schema`, such as the one the mobile guide teaches, still validates on both faces); the `children` / `body` refusals. The document's keys written flat on the node (`navigation`, `title`, `areas`, …) stay undeclared, so the strict face refuses them as unrecognized keys: the document has one spelling, nested. + +```json +{ "type": "app-schema-renderer", "basePath": "/apps/crm", "schema": { "type": "app", "name": "crm", "navigation": [] } } +``` diff --git a/content/docs/guide/layout.md b/content/docs/guide/layout.md index 50eb30b4d3..5943e06c3c 100644 --- a/content/docs/guide/layout.md +++ b/content/docs/guide/layout.md @@ -110,10 +110,42 @@ Two doors remain, one per capability: - **Compose in React** — `` as shown above, with your JSON pages rendered *inside* it through `SchemaRenderer`. -- **The whole shell from metadata** — `AppSchemaRenderer`, registered as - `app-schema-renderer` and declaring its `inputs`, which builds branding and sidebar - navigation from an `AppSchema` JSON document and takes the page content as its - `children`. +- **The whole shell from metadata** — the `app-schema-renderer` node, which renders + `AppSchemaRenderer`. It builds the branding and the sidebar navigation from the app + document it carries under `schema`, below. It draws no page content: the node's + `children` are not rendered, and `objectui validate` refuses them on this node. Page + content reaches the shell only as React `children`, when a host renders + `AppSchemaRenderer` in JSX. + +The node takes three inputs: + +- `schema`: the app document, nested, with its own `"type": "app"`. It is validated as + the app document, so the document's own refusals apply inside it: `mobileNavMode` + there is refused, because the mode belongs on the node. +- `basePath`: the prefix of every href the shell generates. +- `mobileNavMode`: `"drawer"` (the default) or `"bottom_nav"`, which adds a fixed + bottom bar. + +```json +{ + "type": "app-schema-renderer", + "basePath": "/apps/crm", + "mobileNavMode": "bottom_nav", + "schema": { + "type": "app", + "name": "crm", + "title": "CRM", + "navigation": [ + { "id": "accounts", "type": "object", "objectName": "account", "label": "Accounts" } + ] + } +} +``` + +The document has one spelling. Its keys written flat on the node (`"navigation"`, +`"title"` beside `"type": "app-schema-renderer"`) are not drawn, and the strict authoring +face (`StrictAnyComponentSchema` in `@object-ui/types/zod`) refuses them as unrecognized +keys. A node without `schema` draws the shell with no branding and no navigation. ### Features diff --git a/packages/layout/src/__tests__/app-schema-renderer-arm-inputs-11440.test.tsx b/packages/layout/src/__tests__/app-schema-renderer-arm-inputs-11440.test.tsx index 026ddd1492..140ba3d85b 100644 --- a/packages/layout/src/__tests__/app-schema-renderer-arm-inputs-11440.test.tsx +++ b/packages/layout/src/__tests__/app-schema-renderer-arm-inputs-11440.test.tsx @@ -9,23 +9,23 @@ /** * objectui#11440 — the `app-schema-renderer` registration and its zod arm. * - * `@object-ui/types/zod`'s `AppSchemaRendererNodeSchema` declares two of the - * registration's three inputs, each as the registration types it: - * `mobileNavMode` (the same two-value enum) and `basePath` (a string). The - * third input, `schema`, is NOT declared, because no node delivers it: - * `SchemaRenderer` strips the `schema` key and hands the component the NODE as - * its `schema` prop. The last row measures that through the real - * `SchemaRenderer` and registry, so the arm's omission is a reading, not an - * assumption: navigation nested under `schema` draws nothing, and the same - * navigation on the node draws. + * `@object-ui/types/zod`'s `AppSchemaRendererNodeSchema` declares + * `mobileNavMode` (the same two-value enum) and `basePath` (a string), each as + * the registration types it. + * + * The third input, `schema`, moved out of this file ON PURPOSE (objectui#11494, + * triage ruling A `5958227972`). This file measured that no node delivered it: + * `SchemaRenderer` hands a registered component the NODE as its `schema` prop, + * so navigation nested under `schema` drew nothing, and the same navigation on + * the node drew. The registration now goes through an adapter that hands + * `node.schema` to `AppSchemaRenderer`, and the arm declares `schema` as the + * app document by reference, so that row inverted: the nested document draws + * and the flat one does not. Its rows are + * `app-schema-renderer-schema-input-11494.test.tsx`. */ import { describe, it, expect, beforeAll } from 'vitest'; -import React from 'react'; -import { render } from '@testing-library/react'; -import { MemoryRouter } from 'react-router-dom'; import { ComponentRegistry } from '@object-ui/core'; -import { SchemaRenderer } from '@object-ui/react'; import { AppSchemaRendererNodeSchema } from '@object-ui/types/zod'; import { registerLayout } from '../index'; @@ -51,17 +51,4 @@ describe('app-schema-renderer — registration inputs and the arm (objectui#1144 expect(AppSchemaRendererNodeSchema.shape.basePath.safeParse('/apps/crm').success).toBe(true); expect(AppSchemaRendererNodeSchema.shape.basePath.safeParse(7).success).toBe(false); }); - - it('`schema` is an input the arm does not declare — and a node never delivers it to the component', () => { - expect(inputs().map((input) => input.name)).toContain('schema'); - expect(Object.keys(AppSchemaRendererNodeSchema.shape)).not.toContain('schema'); - - const nested = { type: 'app-schema-renderer', schema: { name: 'crm', navigation: [{ id: 'n1', type: 'url', label: 'NestedNav11440', url: '/x' }] } }; - const onNode = { type: 'app-schema-renderer', name: 'crm', navigation: [{ id: 'n2', type: 'url', label: 'NodeNav11440', url: '/y' }] }; - const drawn = (node: unknown) => - render().container.innerHTML; - expect(drawn(nested)).not.toContain('NestedNav11440'); - // Lit control: the same navigation written on the node is drawn. - expect(drawn(onNode)).toContain('NodeNav11440'); - }); }); diff --git a/packages/layout/src/__tests__/app-schema-renderer-schema-input-11494.test.tsx b/packages/layout/src/__tests__/app-schema-renderer-schema-input-11494.test.tsx new file mode 100644 index 0000000000..072ee6e890 --- /dev/null +++ b/packages/layout/src/__tests__/app-schema-renderer-schema-input-11494.test.tsx @@ -0,0 +1,106 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * objectui#11494 — an `app-schema-renderer` node draws the app document it + * carries under `schema` (triage ruling A, comment `5958227972`). + * + * `SchemaRenderer` strips a node's `schema` key out of the props it spreads and + * hands every registered component the NODE as its `schema` prop. Registered + * directly, `AppSchemaRenderer` therefore read the app document's keys off the + * node itself, and the published `schema` input never arrived: objectui#11440 + * measured that a node with its navigation under `schema` drew none of it, + * while the same navigation written on the node drew. That row is inverted + * here, on purpose, under the ruling: the registration now goes through an + * adapter that hands `node.schema` to `AppSchemaRenderer`, so the nested + * document is the one spelling that draws, and the flat one draws nothing. + * `@object-ui/types/zod`'s `AppSchemaRendererNodeSchema` declares `schema` as + * the app document by reference, and the strict face refuses the flat + * spelling; those verdicts are pinned in `@object-ui/types` + * (`app-schema-renderer-schema-input-11494.test.ts`). + * + * Every row renders through the real `SchemaRenderer` and the real registry. + */ + +import { describe, it, expect, beforeAll } from 'vitest'; +import React from 'react'; +import { render } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import { ComponentRegistry } from '@object-ui/core'; +import { SchemaRenderer } from '@object-ui/react'; +import { AppSchemaRendererNodeSchema } from '@object-ui/types/zod'; +import { registerLayout } from '../index'; + +beforeAll(() => { + registerLayout(); +}); + +const NAV = [{ id: 'accounts', type: 'object', objectName: 'account', label: 'Accounts11494' }]; + +const draw = (node: unknown) => + render( + + + , + ).container; + +/** `SchemaErrorBoundary`'s banner, which a throwing render leaves behind. */ +const FAILED_TO_RENDER = 'failed to render'; + +describe('app-schema-renderer — the `schema` input carries the app document (objectui#11494)', () => { + it('a node with its navigation under `schema` draws it', () => { + const container = draw({ type: 'app-schema-renderer', schema: { type: 'app', name: 'crm', navigation: NAV } }); + expect(container.textContent).toContain('Accounts11494'); + expect(container.textContent).not.toContain(FAILED_TO_RENDER); + }); + + it('the same navigation written flat on the node draws nothing: one spelling', () => { + const container = draw({ type: 'app-schema-renderer', name: 'crm', navigation: NAV }); + expect(container.textContent).not.toContain('Accounts11494'); + // The shell itself still draws, so the absence is the navigation's, not the node's. + expect(container.querySelector('[data-sidebar="sidebar"]')).not.toBeNull(); + expect(container.textContent).not.toContain(FAILED_TO_RENDER); + }); + + it('the document draws its branding too: `title` under `schema` heads the sidebar', () => { + const container = draw({ type: 'app-schema-renderer', schema: { type: 'app', name: 'crm', title: 'Title11494' } }); + expect(container.textContent).toContain('Title11494'); + // The flat spelling does not. + expect(draw({ type: 'app-schema-renderer', title: 'Flat11494' }).textContent).not.toContain('Flat11494'); + }); + + it('`basePath` and `mobileNavMode` are unchanged: still read off the node, beside the document', () => { + const container = draw({ + type: 'app-schema-renderer', + basePath: '/apps/crm', + mobileNavMode: 'bottom_nav', + schema: { type: 'app', name: 'crm', navigation: NAV }, + }); + const bar = container.querySelector('[role="navigation"][aria-label="Mobile navigation"]'); + expect(bar).not.toBeNull(); + expect(Array.from(bar!.querySelectorAll('a')).map((a) => a.getAttribute('href'))).toEqual(['/apps/crm/account']); + // Lit control: `drawer`, the default, draws no bar. + const drawer = draw({ type: 'app-schema-renderer', schema: { type: 'app', name: 'crm', navigation: NAV } }); + expect(drawer.querySelector('[aria-label="Mobile navigation"]')).toBeNull(); + }); + + it('a node without `schema` draws the empty shell, as an omitted optional input: no error banner', () => { + // The node `skills/objectui/guides/mobile.md` teaches carries no document. + const container = draw({ type: 'app-schema-renderer', mobileNavMode: 'bottom_nav' }); + expect(container.querySelector('[data-sidebar="sidebar"]')).not.toBeNull(); + expect(container.textContent).not.toContain(FAILED_TO_RENDER); + }); + + it('the registration publishes `schema` as an object input, and the arm declares it', () => { + const inputs = + (ComponentRegistry.getConfig('app-schema-renderer', 'layout') as { inputs?: Array<{ name: string; type: string }> } | undefined) + ?.inputs ?? []; + expect(inputs.find((input) => input.name === 'schema')?.type).toBe('object'); + expect(Object.keys(AppSchemaRendererNodeSchema.shape)).toContain('schema'); + }); +}); diff --git a/packages/layout/src/index.ts b/packages/layout/src/index.ts index b4afad0bff..d66737e2b8 100644 --- a/packages/layout/src/index.ts +++ b/packages/layout/src/index.ts @@ -3,9 +3,11 @@ * Copyright (c) 2024-present ObjectStack Inc. */ +import { createElement, type FC, type ReactNode } from 'react'; import { ComponentRegistry } from '@object-ui/core'; +import type { AppComponentSchema } from '@object-ui/types'; import { PageCard } from './PageCard'; -import { AppSchemaRenderer } from './AppSchemaRenderer'; +import { AppSchemaRenderer, type AppSchemaRendererProps } from './AppSchemaRenderer'; export * from './PageHeader'; export * from './AppShell'; @@ -15,6 +17,49 @@ export * from './ResponsiveGrid'; export * from './NavigationRenderer'; export * from './AppSchemaRenderer'; +/** + * The component the `app-schema-renderer` TAG is registered against: the seam + * that makes the registration's `schema` input true (objectui#11494, triage + * ruling A `5958227972`), in the shape of `@object-ui/plugin-detail`'s + * `DetailSectionNode`. + * + * `SchemaRenderer` strips a node's `schema` key out of the props it spreads, + * and hands every registered component the NODE itself as its `schema` prop. + * Registered directly, `AppSchemaRenderer` therefore took the node for its app + * document: a document nested under `schema` drew an empty shell, and only app + * keys written flat on the node, which nothing declares, drew (measured by + * objectui#11440). This adapter reads the nested document off the node it is + * handed and gives `AppSchemaRenderer` that, so the published input is the one + * spelling that draws. + * + * Why HERE and not in `SchemaRenderer` or `AppSchemaRenderer`: the strip is + * every node's, and `SchemaRenderer` already delivers the node, so reading + * `node.schema` needs no change to it. `AppSchemaRenderer` is a published + * component whose hosts pass `schema={appJson}` in JSX, and none goes through + * the registry; teaching it a node shape would put a second dialect in front of + * them (AGENTS.md #0.1). + * + * ⛔ No fallback to the node's own keys: a node's flat `navigation` / `title` is + * the second spelling the ruling refuses (the strict face refuses it as an + * unrecognized key), so it is not read. A node without `schema` hands the + * component an empty `app` document: the input is optional, as the + * registration declares it, and an omitted document draws the shell with no + * branding and no navigation, which is what such a node drew before. + * `mobileNavMode`, `basePath` and every other prop reach `AppSchemaRenderer` + * unchanged. + */ +type AppSchemaRendererNodeProps = Omit & { + /** The node, as `SchemaRenderer` hands it to every registered component. */ + schema?: { schema?: AppComponentSchema }; + children?: ReactNode; +}; + +/** The document an `app-schema-renderer` node without `schema` draws. */ +const NO_APP_DOCUMENT: AppComponentSchema = { type: 'app' }; + +const AppSchemaRendererNode: FC = ({ schema: node, children, ...shellProps }) => + createElement(AppSchemaRenderer, { ...shellProps, schema: node?.schema ?? NO_APP_DOCUMENT, children }); + export function registerLayout() { // ⛔ The `page-header` node type key is RETIRED (objectui#10859 batch 8, // phase 2c, the seat's fork ruling on that card, by the objectui#10393 / @@ -167,12 +212,17 @@ export function registerLayout() { // node written with this type now renders the OBJUI-001 "Unknown component // type" panel: a loud refusal, not a second source of navigation. - ComponentRegistry.register('app-schema-renderer', AppSchemaRenderer, { + // Registered against `AppSchemaRendererNode` (above), NOT `AppSchemaRenderer`: + // the adapter is what hands the component the `schema` input (objectui#11494). + ComponentRegistry.register('app-schema-renderer', AppSchemaRendererNode, { namespace: 'layout', label: 'App Schema Renderer', category: 'Layout', isContainer: true, inputs: [ + // The app document the shell draws (`@object-ui/types/zod`'s + // `AppComponentSchema`, which `AppSchemaRendererNodeSchema` declares by + // reference). Optional: a node without it draws the shell with no document. { name: 'schema', type: 'object' }, { name: 'basePath', type: 'string' }, // Declared as the vocabulary the renderer implements, not as free text diff --git a/packages/types/src/__tests__/app-mobile-nav-mode-refusal-11363.test.ts b/packages/types/src/__tests__/app-mobile-nav-mode-refusal-11363.test.ts index a24660923b..4030f3a1d6 100644 --- a/packages/types/src/__tests__/app-mobile-nav-mode-refusal-11363.test.ts +++ b/packages/types/src/__tests__/app-mobile-nav-mode-refusal-11363.test.ts @@ -120,10 +120,16 @@ describe('objectui#11363 — the TypeScript face refuses `mobileNavMode` on the }); describe('objectui#11363 — the refusal does not reach an `app-schema-renderer` node', () => { + // The app document is nested under `schema`, its one spelling on this node + // since objectui#11494; written flat, its keys are unrecognized on the strict + // face, which would make every comparison below one against a red baseline. const NODE = { type: 'app-schema-renderer', - title: 'Acme CRM', - navigation: [{ id: 'accounts', type: 'url', label: 'Accounts', url: '/accounts' }], + schema: { + type: 'app', + title: 'Acme CRM', + navigation: [{ id: 'accounts', type: 'url', label: 'Accounts', url: '/accounts' }], + }, }; const verdict = (issues: Issue[] | null) => issues?.map((i) => [i.code, i.path]) ?? null; const LEGAL = VALUES.filter((value) => value !== 'bottom-nav'); diff --git a/packages/types/src/__tests__/app-schema-renderer-schema-input-11494.test.ts b/packages/types/src/__tests__/app-schema-renderer-schema-input-11494.test.ts new file mode 100644 index 0000000000..7a6b936ee1 --- /dev/null +++ b/packages/types/src/__tests__/app-schema-renderer-schema-input-11494.test.ts @@ -0,0 +1,115 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * objectui#11494 — `AppSchemaRendererNodeSchema` declares the registration's + * `schema` input as the app document, BY REFERENCE (triage ruling A, comment + * `5958227972`). + * + * objectui#11440 armed the node with the two inputs that were true then, + * `basePath` and `mobileNavMode`, and left `schema` undeclared because no node + * delivered it. `@object-ui/layout` now registers the node through an adapter + * that hands `node.schema` to `AppSchemaRenderer` (its render rows are + * `app-schema-renderer-schema-input-11494.test.tsx` in that package), so the + * input is true and the arm declares it. Pinned here, on both faces: + * - the member IS `AppComponentSchema`, the same schema object, so there is + * no second copy of the document's members to drift; + * - a node carrying its document under `schema` validates, beside the node's + * own `basePath` and `mobileNavMode`, which are unchanged; + * - one spelling: the same document keys written flat on the node are refused + * at the strict face as unrecognized keys; + * - the document keeps its own refusals: `mobileNavMode` inside `schema` is + * refused by the app document's tombstone (objectui#11363), while the same + * value on the node is clean; + * - the nested document is the `app` document, its `type` included. + * Each refusal has a lit control, so no row passes for the wrong reason. + */ + +import { describe, expect, it } from 'vitest'; +import { + AppComponentSchema, + AppSchemaRendererNodeSchema, + StrictAnyComponentSchema, + safeValidateSchema, +} from '../zod/index.zod'; + +type Issue = { code: string; path: PropertyKey[]; message: string; keys?: string[]; errors?: Issue[][] }; +type Result = { success: boolean; error?: { issues: unknown[] } }; + +const FACES: ReadonlyArray Result]> = [ + ['tolerant', (document) => safeValidateSchema(document)], + ['strict', (document) => StrictAnyComponentSchema.safeParse(document)], +]; +const strict = (document: unknown): Result => StrictAnyComponentSchema.safeParse(document); + +/** Every issue, union branches unfolded and paths made absolute. */ +const allIssues = (issues: Issue[] | undefined, prefix: PropertyKey[] = []): Issue[] => + (issues ?? []).flatMap((issue) => { + const path = [...prefix, ...issue.path]; + return [{ ...issue, path }, ...(issue.errors ?? []).flatMap((branch) => allIssues(branch, path))]; + }); +const issuesOf = (result: Result): Issue[] => allIssues(result.error?.issues as Issue[] | undefined); +const at = (result: Result, path: string): Issue[] => issuesOf(result).filter((issue) => issue.path.join('.') === path); +const show = (result: Result): string => JSON.stringify(result.error?.issues ?? []); + +const NAVIGATION = [{ id: 'accounts', type: 'object', objectName: 'account', label: 'Accounts' }]; +const DOCUMENT = { type: 'app', name: 'acme_crm', title: 'Acme CRM', navigation: NAVIGATION }; +const NODE = { type: 'app-schema-renderer', basePath: '/apps/crm', mobileNavMode: 'bottom_nav', schema: DOCUMENT }; + +describe('`app-schema-renderer` declares `schema` as the app document (objectui#11494)', () => { + it('the member is `AppComponentSchema` itself, by reference, and optional', () => { + const member = AppSchemaRendererNodeSchema.shape.schema as unknown as { unwrap: () => unknown; safeParse: (v: unknown) => { success: boolean } }; + expect(member.unwrap()).toBe(AppComponentSchema); + expect(member.safeParse(undefined).success).toBe(true); + }); + + it.each(FACES)('%s face: a node with its document under `schema` validates, beside `basePath` and `mobileNavMode`', (face, judge) => { + const result = judge(NODE); + expect(result.success, `${face}: ${show(result)}`).toBe(true); + }); + + it.each(FACES)('%s face: a node without `schema` still validates (the node the mobile guide teaches)', (face, judge) => { + const result = judge({ type: 'app-schema-renderer', mobileNavMode: 'bottom_nav' }); + expect(result.success, `${face}: ${show(result)}`).toBe(true); + }); + + it('one spelling: the document\'s keys written flat on the node are refused at the strict face, by name', () => { + const flat = { type: 'app-schema-renderer', title: 'Acme CRM', navigation: NAVIGATION }; + const keys = at(strict(flat), '').filter((issue) => issue.code === 'unrecognized_keys').flatMap((issue) => issue.keys ?? []); + expect(keys.sort()).toEqual(['navigation', 'title']); + // Lit control: the same keys under `schema` are judged by the document, and pass. + expect(strict({ type: 'app-schema-renderer', schema: { type: 'app', title: 'Acme CRM', navigation: NAVIGATION } }).success).toBe(true); + }); + + it.each(FACES)('%s face: the document keeps its own refusals: `mobileNavMode` inside `schema` is the tombstone', (face, judge) => { + const issues = at(judge({ type: 'app-schema-renderer', schema: { ...DOCUMENT, mobileNavMode: 'bottom_nav' } }), 'schema.mobileNavMode'); + expect(issues.map((issue) => issue.code), face).toEqual(['invalid_type']); + expect(issues[0].message).toContain('not a key of the app document'); + // Lit control: the same value on the node, where the mode is read, is clean. + expect(judge(NODE).success, face).toBe(true); + }); + + it.each(FACES)('%s face: the nested document is the `app` document, its `type` included', (face, judge) => { + const { type: _type, ...untyped } = DOCUMENT; + expect(at(judge({ type: 'app-schema-renderer', schema: untyped }), 'schema.type').map((issue) => issue.code), face).toEqual([ + 'invalid_value', + ]); + expect(at(judge({ type: 'app-schema-renderer', schema: 'acme_crm' }), 'schema').map((issue) => issue.code), face).toEqual([ + 'invalid_type', + ]); + }); + + it('the strict face closes the nested document too: an unknown key inside `schema` is refused by name', () => { + const keys = at(strict({ type: 'app-schema-renderer', schema: { ...DOCUMENT, brading: {} } }), 'schema') + .filter((issue) => issue.code === 'unrecognized_keys') + .flatMap((issue) => issue.keys ?? []); + expect(keys).toEqual(['brading']); + // Lit control: the tolerant face keeps the document's passthrough. + expect(safeValidateSchema({ type: 'app-schema-renderer', schema: { ...DOCUMENT, brading: {} } }).success).toBe(true); + }); +}); diff --git a/packages/types/src/__tests__/passing-keys-arms-11440.test.ts b/packages/types/src/__tests__/passing-keys-arms-11440.test.ts index 18d71fdbad..b51d35bb4d 100644 --- a/packages/types/src/__tests__/passing-keys-arms-11440.test.ts +++ b/packages/types/src/__tests__/passing-keys-arms-11440.test.ts @@ -154,12 +154,13 @@ describe('`app-schema-renderer` (objectui#11440)', () => { } }); - it('the registration\'s `schema` input is not declared: no node delivers it (measured on objectui#11440)', () => { - expect(Object.keys(AppSchemaRendererNodeSchema.shape)).not.toContain('schema'); - const node = { type: 'app-schema-renderer', schema: { name: 'crm' } }; - // Tolerant face: unjudged, as every undeclared key. Strict face: refused as an unrecognized key. - expect(safeValidateSchema(node).success).toBe(true); - expect(issuesOf(StrictAnyComponentSchema.safeParse(node)).flatMap((i) => i.keys ?? [])).toEqual(['schema']); + // The registration's third input, `schema`, was not declared here because no + // node delivered it (measured on objectui#11440). objectui#11494 (triage + // ruling A `5958227972`) made it true through a registration adapter and + // declared it as the app document by reference, so this row moved, inverted, + // to `./app-schema-renderer-schema-input-11494.test.ts`. + it('the registration\'s `schema` input is declared since objectui#11494 (its rows moved there)', () => { + expect(Object.keys(AppSchemaRendererNodeSchema.shape)).toContain('schema'); }); }); diff --git a/packages/types/src/__tests__/zod-mirror-parity.test.ts b/packages/types/src/__tests__/zod-mirror-parity.test.ts index 57e3dcecfb..e9ce19c518 100644 --- a/packages/types/src/__tests__/zod-mirror-parity.test.ts +++ b/packages/types/src/__tests__/zod-mirror-parity.test.ts @@ -4713,9 +4713,11 @@ const EXCLUSIONS: Readonly> = { 'app.zod.ts#MenuItemSchema': "recursive; a `z.lazy` exposes no `.shape` to read, so there is no key set for the per-key comparison. Since objectui#7760 it carries its TS declaration as BOTH type arguments, so the pair IS compared — as a whole type, by `tsc`, at the annotation itself", // objectui#11440 — the `app-schema-renderer` whole-shell node, declared from - // its registration `inputs`. Pinned by `./passing-keys-arms-11440.test.ts`. + // its registration `inputs`. Pinned by `./passing-keys-arms-11440.test.ts`; + // its `schema` member (objectui#11494) by + // `./app-schema-renderer-schema-input-11494.test.ts`. 'app.zod.ts#AppSchemaRendererNodeSchema': - "registration-owned — `BaseSchema` + the `app-schema-renderer` literal + two of the registration's three `inputs` (`basePath`, `mobileNavMode`; the third, `schema`, is stripped by `SchemaRenderer` and never reaches the component) + the objectui#9256 `body` / `children` refusals; no TS declaration in this package restates the node (`@object-ui/layout`'s `AppSchemaRendererProps` is the component's prop type)", + "registration-owned — `BaseSchema` + the `app-schema-renderer` literal + the registration's three `inputs`: `schema`, which IS `app.zod.ts#AppComponentSchema` (the same schema object, optional; `@object-ui/layout`'s registration adapter hands it to the component, objectui#11494), `basePath` and `mobileNavMode` + the objectui#9256 `body` / `children` refusals; no TS declaration in this package restates the node (`@object-ui/layout`'s `AppSchemaRendererProps` is the component's prop type)", 'app.zod.ts#AppContextSelectorSchema': "spec-owned BY REFERENCE — the local `.extend(…)` adds renderer props that no TS declaration in this package restates", 'base.zod.ts#SchemaNodeSchema': diff --git a/packages/types/src/zod/README.md b/packages/types/src/zod/README.md index 5bfbdd76cd..8a398e1d6b 100644 --- a/packages/types/src/zod/README.md +++ b/packages/types/src/zod/README.md @@ -272,7 +272,7 @@ Every arm except the four `page:` containers (`page:card`, `page:section`, `page The last six arrived with `@objectstack/spec` 17.5.0, whose rows for them were measured at their renderers' reads (objectui#10872 batch 4). Their content channels were measured by the objectui#9256 method in objectui#10872 batch 5, and none of their renderers reads either one, so each refuses `children` and `body` like the arms above. The message names where the content goes instead: `properties.actions` for `action:group` and `action:menu`, `properties.items` for `element:definition-list`, and the label and icon for the two buttons. `element:repeater` has no content channel, and its message says so. `ActionButtonBlockSchema` and `ActionIconBlockSchema` also refuse two keys their renderers read off the node: `onClick`, a runtime slot for a host-supplied function (objectui#6124), and a flat `onSuccess`, pointed at `properties.onSuccess`, the row's member. ### Shell and Section Nodes -- `AppSchemaRendererNodeSchema` — `app-schema-renderer`, `@object-ui/layout`'s whole-shell node (objectui#11440). It declares two of its registration's inputs: `mobileNavMode` (`'drawer'` | `'bottom_nav'`) and `basePath`. The third input, `schema`, is not declared, because `SchemaRenderer` strips that key and the component reads the app document off the node itself. Both content channels are refused: a node draws neither. +- `AppSchemaRendererNodeSchema` — `app-schema-renderer`, `@object-ui/layout`'s whole-shell node (objectui#11440). It declares its registration's three inputs: `schema`, the app document the shell draws, which is `AppComponentSchema` itself, by reference (objectui#11494); `mobileNavMode` (`'drawer'` | `'bottom_nav'`); and `basePath`. The document is nested, `{ "type": "app-schema-renderer", "schema": { "type": "app", "navigation": [...] } }`, and keeps its own refusals there (`mobileNavMode` inside `schema` is refused, objectui#11363). Its keys written flat on the node are refused by the strict face as unrecognized keys, and the node does not draw them: one spelling. Both content channels are refused: a node draws neither. - `DetailSectionNodeSchema` — `detail-section`, one field section of `@object-ui/plugin-detail` as a node (objectui#11440): the ten `DetailViewSectionSchema` members its registration publishes, flat on the node, `fields` required. It is not `record:details`'s section shape (that one names its heading `label` and takes field names), so it is a node of its own. ### Cloud Widgets (1) diff --git a/packages/types/src/zod/app.zod.ts b/packages/types/src/zod/app.zod.ts index f58688ea03..fef29225bd 100644 --- a/packages/types/src/zod/app.zod.ts +++ b/packages/types/src/zod/app.zod.ts @@ -706,30 +706,45 @@ const APP_SCHEMA_RENDERER_NEITHER_CHANNEL = * literal, so `objectui validate` refused that node with `invalid_union` at * `type`. * - * ## The members, and the input that is NOT one + * ## The members * * The registration declares three `inputs`: `schema` (object), `basePath` - * (string) and `mobileNavMode` (the enum `'drawer'` | `'bottom_nav'`). Two are - * declared here, each as the registration types it: + * (string) and `mobileNavMode` (the enum `'drawer'` | `'bottom_nav'`). All + * three are declared here: + * - `schema` — the app document the shell draws, as {@link AppComponentSchema} + * itself, by reference (objectui#11494, triage ruling A `5958227972`). * - `mobileNavMode` — the two modes the renderer implements; `'bottom_nav'` * adds the fixed bottom bar. * - `basePath` — the prefix of every href the shell generates. * - * ⛔ `schema` is NOT declared, because no node delivers it. `SchemaRenderer` - * strips the `schema` key out of the props it spreads and hands the component - * the NODE as its `schema` prop, so `AppSchemaRenderer` reads the app document - * keys off the node itself and never the nested `schema` object. Measured - * through the real `SchemaRenderer` and registry (objectui#11440): a node with - * its navigation nested under `schema` drew no navigation, and the same - * navigation written on the node drew. Where the app document belongs on this - * node is left to the seat, and declaring `schema` would publish an input - * that renders nothing. + * ## `schema` is the document, nested: one spelling (objectui#11494) + * + * `SchemaRenderer` strips the `schema` key out of the props it spreads and + * hands every registered component the NODE as its `schema` prop. Registered + * directly, `AppSchemaRenderer` therefore read the document's keys off the node + * itself, and objectui#11440 measured through the real `SchemaRenderer` and + * registry that a node with its navigation nested under `schema` drew none of + * it. `@object-ui/layout` now registers the node through an adapter that hands + * `node.schema` to `AppSchemaRenderer`, so the nested document is what draws. + * + * The member is `AppComponentSchema` and not a copy of its members, so the + * document keeps its own refusals here: `mobileNavMode` inside `schema` is + * refused by {@link APP_MOBILE_NAV_MODE_REFUSAL}, while the same key on the node + * is the mode. The document is the `app` document, so its `type` is `'app'`. + * The document's keys written flat on the node are not declared, so the strict + * face refuses them as unrecognized keys, and the adapter does not read them: + * the document has one spelling. The member is optional, as the registration's + * input is: a node without it draws the shell with no document (no branding, + * no navigation), which is what the mobile guide's node draws. * * Neither content channel is read (see * {@link APP_SCHEMA_RENDERER_NEITHER_CHANNEL}), so both are refused by name. */ export const AppSchemaRendererNodeSchema = BaseSchema.extend({ type: z.literal('app-schema-renderer'), + schema: AppComponentSchema.optional().describe( + 'The app document the shell draws (branding, `navigation`, `areas`), nested: `{ "type": "app", ... }`', + ), basePath: z.string().optional().describe('URL prefix for the hrefs the shell generates (e.g. "/apps/crm")'), mobileNavMode: z .enum(['drawer', 'bottom_nav']) From ebb7d21ba936a0ff6ef5f9d7f619c94130410c0e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Oct 2026 18:25:29 +0000 Subject: [PATCH 2/5] docs(types): name the app arm in the zod README without the type literal the actions census counts The handler-keys census counts files that name `AppComponentSchema` and read `.actions`; the README already carries `properties.actions`, so naming the type there added a third prose file to its population. Claude-Session: https://claude.ai/code/session_01YLg8XqWGJ785fwQ5v4pH37 Co-authored-by: Claude --- packages/types/src/zod/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/types/src/zod/README.md b/packages/types/src/zod/README.md index 8a398e1d6b..0c9641b6d8 100644 --- a/packages/types/src/zod/README.md +++ b/packages/types/src/zod/README.md @@ -272,7 +272,7 @@ Every arm except the four `page:` containers (`page:card`, `page:section`, `page The last six arrived with `@objectstack/spec` 17.5.0, whose rows for them were measured at their renderers' reads (objectui#10872 batch 4). Their content channels were measured by the objectui#9256 method in objectui#10872 batch 5, and none of their renderers reads either one, so each refuses `children` and `body` like the arms above. The message names where the content goes instead: `properties.actions` for `action:group` and `action:menu`, `properties.items` for `element:definition-list`, and the label and icon for the two buttons. `element:repeater` has no content channel, and its message says so. `ActionButtonBlockSchema` and `ActionIconBlockSchema` also refuse two keys their renderers read off the node: `onClick`, a runtime slot for a host-supplied function (objectui#6124), and a flat `onSuccess`, pointed at `properties.onSuccess`, the row's member. ### Shell and Section Nodes -- `AppSchemaRendererNodeSchema` — `app-schema-renderer`, `@object-ui/layout`'s whole-shell node (objectui#11440). It declares its registration's three inputs: `schema`, the app document the shell draws, which is `AppComponentSchema` itself, by reference (objectui#11494); `mobileNavMode` (`'drawer'` | `'bottom_nav'`); and `basePath`. The document is nested, `{ "type": "app-schema-renderer", "schema": { "type": "app", "navigation": [...] } }`, and keeps its own refusals there (`mobileNavMode` inside `schema` is refused, objectui#11363). Its keys written flat on the node are refused by the strict face as unrecognized keys, and the node does not draw them: one spelling. Both content channels are refused: a node draws neither. +- `AppSchemaRendererNodeSchema` — `app-schema-renderer`, `@object-ui/layout`'s whole-shell node (objectui#11440). It declares its registration's three inputs: `schema`, the app document the shell draws, which is the `app` document's own arm in `app.zod.ts`, the same schema object, by reference (objectui#11494); `mobileNavMode` (`'drawer'` | `'bottom_nav'`); and `basePath`. The document is nested, `{ "type": "app-schema-renderer", "schema": { "type": "app", "navigation": [...] } }`, and keeps its own refusals there (`mobileNavMode` inside `schema` is refused, objectui#11363). Its keys written flat on the node are refused by the strict face as unrecognized keys, and the node does not draw them: one spelling. Both content channels are refused: a node draws neither. - `DetailSectionNodeSchema` — `detail-section`, one field section of `@object-ui/plugin-detail` as a node (objectui#11440): the ten `DetailViewSectionSchema` members its registration publishes, flat on the node, `fields` required. It is not `record:details`'s section shape (that one names its heading `label` and takes field names), so it is a node of its own. ### Cloud Widgets (1) From 25c91e58be7fdaf11657c03ddbebcf489fabede1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Oct 2026 18:45:39 +0000 Subject: [PATCH 3/5] docs(layout): the app-schema-renderer example's navigation entry is a page entry The doc component-type gate reads every `type` literal in a fence, and `object` is a navigation kind the gate exempts per page; layout.md carries no such exemption. A `page` entry teaches the same nesting with a key the gate resolves. Claude-Session: https://claude.ai/code/session_01YLg8XqWGJ785fwQ5v4pH37 Co-authored-by: Claude --- content/docs/guide/layout.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/docs/guide/layout.md b/content/docs/guide/layout.md index 5943e06c3c..50eff212dd 100644 --- a/content/docs/guide/layout.md +++ b/content/docs/guide/layout.md @@ -136,7 +136,7 @@ The node takes three inputs: "name": "crm", "title": "CRM", "navigation": [ - { "id": "accounts", "type": "object", "objectName": "account", "label": "Accounts" } + { "id": "home", "type": "page", "pageName": "home", "label": "Home" } ] } } From 44a67d8fce7974a65ae15a7db62669cc1d31e317 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Oct 2026 18:51:17 +0000 Subject: [PATCH 4/5] test(schema-catalog): the app-schema-renderer control node nests a typed app document The fixture's document had no `type: 'app'`, which AppSchemaRendererNodeSchema now judges as the app document and refuses at `schema.type`. The manifest reading the test makes is unchanged; the node is now clean on every face. Claude-Session: https://claude.ai/code/session_01YLg8XqWGJ785fwQ5v4pH37 Co-authored-by: Claude --- .../schema-catalog/test/pageheader-with-actions.test.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/examples/schema-catalog/test/pageheader-with-actions.test.tsx b/examples/schema-catalog/test/pageheader-with-actions.test.tsx index 35a8e4d8ba..349255d386 100644 --- a/examples/schema-catalog/test/pageheader-with-actions.test.tsx +++ b/examples/schema-catalog/test/pageheader-with-actions.test.tsx @@ -280,8 +280,13 @@ describe('the retired `page-header` alias is unknown to the manifest (objectui#1 }); }); -/** The layout key the same manifest still resolves — the lit control below. */ -const APP_SCHEMA_NODE = { type: 'app-schema-renderer', schema: { name: 'crm', navigation: [] } }; +/** + * The layout key the same manifest still resolves — the lit control below. Its + * app document is nested under `schema` with its own `type: 'app'`, the one + * spelling `AppSchemaRendererNodeSchema` accepts since objectui#11494, so the + * node is clean on every face, not only in this manifest. + */ +const APP_SCHEMA_NODE = { type: 'app-schema-renderer', schema: { type: 'app', name: 'crm', navigation: [] } }; describe('the retired `navigation-renderer` and `responsive-grid` keys are unknown to the manifest (objectui#11441)', () => { it('draws `unknown-component` for both spellings of `navigation-renderer`', () => { From e5dc2df3028adfdc110fd2ba2bf861a0933eb5b4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Oct 2026 19:36:46 +0000 Subject: [PATCH 5/5] fix(types): name the app-schema-renderer arm's type so declaration emit refers to the nested app document by name Nesting AppComponentSchema into the app-schema-renderer arm made declaration emit re-serialize the whole app document a second time inside AnyComponentSchema, and tsc refused the union with TS7056 against @objectstack/spec built from objectstack main (Spec Main Shape Gate). The arm is now annotated with AppSchemaRendererNodeSchemaType, written by reference to AppComponentSchema, on the PageKindNodeSchemaType precedent. Type only: the schema and its accept set are unchanged. Appends a dated note to the pending objectui#11440 changeset: its sentence that the registration's `schema` input is not declared no longer holds. Claude-Session: https://claude.ai/code/session_01YLg8XqWGJ785fwQ5v4pH37 Co-authored-by: Claude --- .changeset/11440-arm-passing-types.md | 9 +++++++++ packages/types/src/zod/app.zod.ts | 26 +++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/.changeset/11440-arm-passing-types.md b/.changeset/11440-arm-passing-types.md index e7690bc29a..994b8e91c8 100644 --- a/.changeset/11440-arm-passing-types.md +++ b/.changeset/11440-arm-passing-types.md @@ -18,3 +18,12 @@ The strict authoring face (`StrictAnyComponentSchema`) derives from these arms and accepts the same documents, closed to undeclared keys. **The TypeScript face.** `@object-ui/types` exports `ObjectPivotBlockNode` and `EmbeddableFormBlockNode`, the two arms' authoring inputs, derived by reference as the other `ObjectQLPublicBlockNode` members are, so they are in `AuthoringNode` and `SchemaRenderer`'s `schema` prop accepts them. Each bag is required and closed: a misspelled bag key, a missing bag and a prop written flat do not compile. The page kinds were already typed (`PageDocumentNode`, the spec's own page input). + +⚠️ **Dated note, 2026-10-02 — `schema` is now declared — objectui#11494.** +Later in this same release the registration's third input, `schema`, is +declared on `AppSchemaRendererNodeSchema` as the app document, by reference +(`AppComponentSchema` itself, optional), and `@object-ui/layout`'s +registration adapter delivers it: an `app-schema-renderer` node draws the +document nested under `schema`, and the strict face accepts it there. The +sentence above that says `schema` is not declared, and that the strict face +refuses it as an unrecognized key, no longer describes the node. The rest of this entry is kept as the reading of this change. diff --git a/packages/types/src/zod/app.zod.ts b/packages/types/src/zod/app.zod.ts index fef29225bd..074f0be2c0 100644 --- a/packages/types/src/zod/app.zod.ts +++ b/packages/types/src/zod/app.zod.ts @@ -740,7 +740,7 @@ const APP_SCHEMA_RENDERER_NEITHER_CHANNEL = * Neither content channel is read (see * {@link APP_SCHEMA_RENDERER_NEITHER_CHANNEL}), so both are refused by name. */ -export const AppSchemaRendererNodeSchema = BaseSchema.extend({ +export const AppSchemaRendererNodeSchema: AppSchemaRendererNodeSchemaType = BaseSchema.extend({ type: z.literal('app-schema-renderer'), schema: AppComponentSchema.optional().describe( 'The app document the shell draws (branding, `navigation`, `areas`), nested: `{ "type": "app", ... }`', @@ -757,6 +757,30 @@ export const AppSchemaRendererNodeSchema = BaseSchema.extend({ children: retirementTombstone(APP_SCHEMA_RENDERER_NEITHER_CHANNEL), }); +/** + * The TYPE of {@link AppSchemaRendererNodeSchema}, written out BY REFERENCE to + * {@link AppComponentSchema} for its `schema` member, on the precedent of + * `PageKindNodeSchemaType` in `layout.zod.ts` (objectui#11440). Without it, + * declaration emit re-serializes the whole app document a second time inside + * `AnyComponentSchema`, beside the `app` arm, and `tsc` refuses that union with + * TS7056 ("The inferred type of this node exceeds the maximum length the + * compiler will serialize"), measured on objectui#11494 against + * `@objectstack/spec` built from objectstack `main` (the `Spec Main Shape + * Gate`). A named type is emitted by name. It is a type only: the schema above + * and what it accepts are unchanged. + */ +export type AppSchemaRendererNodeSchemaType = z.ZodObject< + Omit<(typeof BaseSchema)['shape'], 'type' | 'body' | 'children'> & { + type: z.ZodLiteral<'app-schema-renderer'>; + schema: z.ZodOptional; + basePath: z.ZodOptional; + mobileNavMode: z.ZodOptional>; + body: ReturnType; + children: ReturnType; + }, + z.core.$loose +>; + /** * Export type inference helpers */