diff --git a/.changeset/ai-chat-window-retired.md b/.changeset/ai-chat-window-retired.md new file mode 100644 index 00000000000..4aaed4b92a2 --- /dev/null +++ b/.changeset/ai-chat-window-retired.md @@ -0,0 +1,43 @@ +--- +'@objectstack/spec': minor +--- + +feat(spec)!: `ai:chat_window` is retired — refused by name at the schema door, the floating chat overlay is the AI chat entry point (#21504, ADR-0049) + +Clause-②: yes (narrowing) + + + +**BREAKING** — an accept-set narrowing on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings (the `user:profile`, `element:filter` and `element:form` retirements shipped the same way). + +`ai:chat_window` was declared in `PageComponentType` and mapped to `AIChatWindowProps` (`mode`, `agentId`, `context`, `aria`) in `ComponentPropsMap`, and no renderer for it ever shipped — not in objectui, framework or cloud. The console leaves it unregistered on purpose: the floating chat overlay it mounts on every page is the supported AI chat entry point, and an inline page-level chat window is not part of the supported surface. So an authored `ai:chat_window` node validated clean and then drew "Unknown component type" in front of an end user, and none of its four props configured anything. The triage ruling retired it under ADR-0049 enforce-or-remove, refused by name, following the `user:profile` precedent. + +**What is refused:** an authored `ai:chat_window` component node, at `PageComponentSchema.type`. That covers `definePage()`, `PageSchema`, and every door that parses pages: `os validate`, `os build`, `os lint` and the metadata save door. The issue is located at the node's own path, with `code: 'custom'` and `params.retiredComponentType`, and its message is the retirement prescription. `PageComponentType`'s own error map refuses the name with the same text when the enum is parsed alone. `ComponentPropsMap['ai:chat_window']` stays as a row, so every reader that dispatches on it keeps recognising the name: the component-props gate, `check-yaml-examples` and the type vocabulary's known set. The row now refuses every props bag, `{}` included, with the same prescription. One prescription string, `RETIRED_PAGE_COMPONENT_TYPES` in `@objectstack/spec/ui`, answers at all three doors. + +**What is removed from the exports:** `AIChatWindowProps` (`@objectstack/spec/ui`), the props schema the element no longer has. Its JSON Schema (`ui/AIChatWindowProps`) is no longer published. + +**What stays accepted:** every other member of `PageComponentType` and `ComponentPropsMap`, byte-identically. That includes `ai:suggestion`, which keeps its row and its place in the enum, so `ai:` stays a namespace the `component-type-unknown` authoring rule claims. The open string arm also stays open: custom and plugin-registered types keep parsing. The only string refused is the retired name itself. + +## FROM → TO + +| you wrote | write instead | +|:--|:--| +| a `{ type: 'ai:chat_window' }` component node in a page region, slot or container | nothing: delete the node. The floating chat overlay is on every page already | +| `properties: { agentId: '…' }` on that node | the app's `defaultAgent` (a platform agent: `ask`, the default, or `build` on an authoring surface) | +| `properties: { mode, context, aria }` on that node | nothing: none of them was ever read, and the overlay is not configured per page | + +The one-line fix: delete the `ai:chat_window` component node. No ADR-0087 conversion is registered, because the only edit is deleting an authored page node, and a mechanical conversion does not delete page nodes: which region closes up is a layout decision. The D3 entry `ui-ai-chat-window-retired` carries that delegation, so `os migrate meta --from 17` lists it as a manual change for every stack that still names the type. + +## Who is affected, measured + +- **objectstack** at `529d9711fb`: zero authored `ai:chat_window` nodes in `examples/**`, `packages/apps/**`, `apps/**`, `skills/**` and `content/docs/**` code samples. The only hits were the spec's own type list, its row, its tests and the generated reference docs. The control in the same query shape: `element:divider` is authored in 3 example files and `record:details` in 12. +- **objectui** at the `.objectui-sha` pin `89cad75d55`: no renderer is registered. `components/src/renderers/placeholders.tsx` omits the type on purpose, and Studio's page palette excludes it. The remaining hits are tests asserting its absence, the palette exclusion, a parity-ledger entry and comments. No non-test source imports `AIChatWindowProps` or indexes the row. +- **cloud** and **hotcrm** (triage's census): zero producers. hotcrm names it once, in a comment, as dropped. +- **Deployed metadata** was not measured. + +The retirement kit: + +- the retired-type map entry, the enum value removed (`packages/spec/src/ui/page.zod.ts`), and the row turned into a whole-bag refusal, with `AIChatWindowProps` removed (`packages/spec/src/ui/component.zod.ts`) +- the D3 semantic entry `ui-ai-chat-window-retired`, its step-18 rationale fragment, and the `RETIRED_DEFS_BY_MAJOR` entry `ui/AIChatWindowProps` +- pin tests: in `component.test.ts`, `code`, `path`, `params` and the first sentence at each of the three doors, with `ai:suggestion` as the control and the open arm left open. In `component-type-vocabulary.test.ts`, the type stays known, leaves the typo candidates, and `ai:` stays reserved. The `ComponentPropsMap` `z.unknown()` enumeration loses its `ai:chat_window` `context` line with the row's keys. +- generated baselines and docs follow the schema: `api-surface/`, `export-origins/`, `declaration-map/`, `authorable-surface/`, `authorable-defaults/`, `json-schema.manifest/`, `spec-changes.json`, the upgrade guide and the reference docs. The hand-written `content/docs/ui/pages.mdx` component list now says the truth. diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx index f0e035eccf9..70735ca465b 100644 --- a/content/docs/references/index.mdx +++ b/content/docs/references/index.mdx @@ -1,7 +1,7 @@ --- title: Protocol reference — every schema by module navTitle: Protocol Reference -description: Every schema published by @objectstack/spec — 1517 schemas across 14 protocol modules +description: Every schema published by @objectstack/spec — 1516 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/. */} @@ -33,8 +33,8 @@ counts are sums of the rows they head. Regenerate with | [Shared Protocol](/docs/references/shared) | 10 | 31 | Primitives used across every protocol — identifiers, HTTP, expressions, error maps, enums. | | [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. | | [System Protocol](/docs/references/system) | 34 | 275 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. | -| [UI Protocol](/docs/references/ui) | 16 | 166 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | -| **Total** | **196** | **1517** | 14 protocol modules | +| [UI Protocol](/docs/references/ui) | 16 | 165 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | +| **Total** | **196** | **1516** | 14 protocol modules | --- @@ -363,7 +363,7 @@ The runtime environment — logging, jobs, cache, metrics, notifications, i18n a ## UI Protocol -**Source:** `packages/spec/src/ui/` · **Import:** `@objectstack/spec/ui` · **16 pages, 166 schemas** +**Source:** `packages/spec/src/ui/` · **Import:** `@objectstack/spec/ui` · **16 pages, 165 schemas** Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. @@ -374,7 +374,7 @@ Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI lay | [`app.zod.ts`](/docs/references/ui/app) | `ActionNavItem`, `App`, `AppBranding`, `AppContextSelector`, `ComponentNavItem`, `DashboardNavItem`, `DocNavItem`, `GroupNavItem`, `NavigationArea`, `NavigationContribution`, `NavigationItem`, `ObjectNavItem`, `PageNavItem`, `ReportNavItem`, `UrlNavItem` | | [`bulk-action.zod.ts`](/docs/references/ui/bulk-action) | `BulkActionDef`, `BulkActionExecution`, `BulkActionOperation`, `BulkActionParam` | | [`chart.zod.ts`](/docs/references/ui/chart) | `ChartAggregate`, `ChartAggregateFunction`, `ChartAnnotation`, `ChartAxis`, `ChartConfig`, `ChartDrillDown`, `ChartGroupBy`, `ChartInteraction`, `ChartSeries`, `ChartType` | -| [`component.zod.ts`](/docs/references/ui/component) | `AIChatWindowProps`, `ActionButtonProps`, `ActionGroupProps`, `ActionIconProps`, `ActionMenuProps`, `ElementButtonProps`, `ElementDefinitionListProps`, `ElementFilterProps`, `ElementFormProps`, `ElementImageProps`, `ElementMetadataViewerProps`, `ElementNumberProps`, `ElementRecordPickerProps`, `ElementRepeaterProps`, `ElementTextInputProps`, `ElementTextProps`, `ObjectCalendarProps`, `ObjectFormProps`, `ObjectGanttProps`, `ObjectGridProps`, `ObjectKanbanProps`, `ObjectMapProps`, `ObjectMasterDetailFormProps`, `ObjectMetricProps`, `ObjectTimelineProps`, `ObjectTreeProps`, `PageAccordionProps`, `PageCardProps`, `PageContainerProps`, `PageHeaderProps`, `PageTabsProps`, `RecordActivityProps`, `RecordAlertAction`, `RecordAlertProps`, `RecordChatterProps`, `RecordDetailsProps`, `RecordHighlightsField`, `RecordHighlightsProps`, `RecordHistoryProps`, `RecordLineItemsProps`, `RecordPathProps`, `RecordQuickActionsProps`, `RecordReferenceRailProps`, `RecordRelatedListProps`, `ReferenceRailEntry` | +| [`component.zod.ts`](/docs/references/ui/component) | `ActionButtonProps`, `ActionGroupProps`, `ActionIconProps`, `ActionMenuProps`, `ElementButtonProps`, `ElementDefinitionListProps`, `ElementFilterProps`, `ElementFormProps`, `ElementImageProps`, `ElementMetadataViewerProps`, `ElementNumberProps`, `ElementRecordPickerProps`, `ElementRepeaterProps`, `ElementTextInputProps`, `ElementTextProps`, `ObjectCalendarProps`, `ObjectFormProps`, `ObjectGanttProps`, `ObjectGridProps`, `ObjectKanbanProps`, `ObjectMapProps`, `ObjectMasterDetailFormProps`, `ObjectMetricProps`, `ObjectTimelineProps`, `ObjectTreeProps`, `PageAccordionProps`, `PageCardProps`, `PageContainerProps`, `PageHeaderProps`, `PageTabsProps`, `RecordActivityProps`, `RecordAlertAction`, `RecordAlertProps`, `RecordChatterProps`, `RecordDetailsProps`, `RecordHighlightsField`, `RecordHighlightsProps`, `RecordHistoryProps`, `RecordLineItemsProps`, `RecordPathProps`, `RecordQuickActionsProps`, `RecordReferenceRailProps`, `RecordRelatedListProps`, `ReferenceRailEntry` | | [`dashboard.zod.ts`](/docs/references/ui/dashboard) | `Dashboard`, `DashboardHeader`, `DashboardHeaderAction`, `DashboardWidget`, `DashboardWidgetChartConfig`, `DashboardWidgetOptions`, `GlobalFilter`, `GlobalFilterOptionsFrom`, `WidgetActionType`, `WidgetColorVariant` | | [`dataset.zod.ts`](/docs/references/ui/dataset) | `Dataset`, `DatasetDimension`, `DatasetMeasure`, `DerivedMeasureOp` | | [`expression-bindable-text-keys.zod.ts`](/docs/references/ui/expression-bindable-text-keys) | `ExpressionBindableTextKey` | diff --git a/content/docs/references/ui/component.mdx b/content/docs/references/ui/component.mdx index bd4a6c69daf..7d034275fca 100644 --- a/content/docs/references/ui/component.mdx +++ b/content/docs/references/ui/component.mdx @@ -1,7 +1,7 @@ --- title: Component schema — UI Protocol reference navTitle: Component -description: "Component schemas of the ObjectStack UI Protocol: AIChatWindowProps and 44 more — each property with its type, default and a TypeScript example." +description: "Component schemas of the ObjectStack UI Protocol: ActionButtonProps and 43 more — each property with its type, default and a TypeScript example." --- {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} @@ -13,35 +13,13 @@ description: "Component schemas of the ObjectStack UI Protocol: AIChatWindowProp ## TypeScript Usage ```typescript -import { AIChatWindowProps, ActionButtonPropsSchema, ActionGroupPropsSchema, ActionIconPropsSchema, ActionMenuPropsSchema, ElementButtonPropsSchema, ElementDefinitionListPropsSchema, ElementFilterPropsSchema, ElementFormPropsSchema, ElementImagePropsSchema, ElementMetadataViewerPropsSchema, ElementNumberPropsSchema, ElementRecordPickerPropsSchema, ElementRepeaterPropsSchema, ElementTextInputPropsSchema, ElementTextPropsSchema, ObjectCalendarPropsSchema, ObjectFormPropsSchema, ObjectGanttPropsSchema, ObjectGridPropsSchema, ObjectKanbanPropsSchema, ObjectMapPropsSchema, ObjectMasterDetailFormPropsSchema, ObjectMetricPropsSchema, ObjectTimelinePropsSchema, ObjectTreePropsSchema, PageAccordionProps, PageCardProps, PageContainerProps, PageHeaderProps, PageTabsProps, RecordActivityProps, RecordAlertActionSchema, RecordAlertProps, RecordChatterProps, RecordDetailsProps, RecordHighlightsField, RecordHighlightsProps, RecordHistoryProps, RecordLineItemsProps, RecordPathProps, RecordQuickActionsProps, RecordReferenceRailProps, RecordRelatedListProps, ReferenceRailEntrySchema } from '@objectstack/spec/ui'; +import { ActionButtonPropsSchema, ActionGroupPropsSchema, ActionIconPropsSchema, ActionMenuPropsSchema, ElementButtonPropsSchema, ElementDefinitionListPropsSchema, ElementFilterPropsSchema, ElementFormPropsSchema, ElementImagePropsSchema, ElementMetadataViewerPropsSchema, ElementNumberPropsSchema, ElementRecordPickerPropsSchema, ElementRepeaterPropsSchema, ElementTextInputPropsSchema, ElementTextPropsSchema, ObjectCalendarPropsSchema, ObjectFormPropsSchema, ObjectGanttPropsSchema, ObjectGridPropsSchema, ObjectKanbanPropsSchema, ObjectMapPropsSchema, ObjectMasterDetailFormPropsSchema, ObjectMetricPropsSchema, ObjectTimelinePropsSchema, ObjectTreePropsSchema, PageAccordionProps, PageCardProps, PageContainerProps, PageHeaderProps, PageTabsProps, RecordActivityProps, RecordAlertActionSchema, RecordAlertProps, RecordChatterProps, RecordDetailsProps, RecordHighlightsField, RecordHighlightsProps, RecordHistoryProps, RecordLineItemsProps, RecordPathProps, RecordQuickActionsProps, RecordReferenceRailProps, RecordRelatedListProps, ReferenceRailEntrySchema } from '@objectstack/spec/ui'; import type { ActionButtonProps, ActionGroupProps, ActionIconProps, ActionMenuProps, ElementDefinitionListProps, ElementNumberProps, ElementRecordPickerProps, ElementRepeaterProps, ObjectCalendarProps, ObjectFormProps, ObjectGanttProps, ObjectGridProps, ObjectKanbanProps, ObjectMapProps, ObjectMasterDetailFormProps, ObjectMetricProps, ObjectTimelineProps, ObjectTreeProps, PageContainerProps, RecordAlertAction, RecordAlertProps, RecordHighlightsField, RecordHistoryProps, RecordLineItemsProps, RecordPathProps, RecordQuickActionsProps, RecordReferenceRailProps, ReferenceRailEntry } from '@objectstack/spec/ui'; // Validate data -const result = AIChatWindowProps.parse(data); +const result = ActionButtonPropsSchema.parse(data); ``` ---- - -## AIChatWindowProps - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **mode** | `Enum<'float' \| 'sidebar' \| 'inline'>` | optional (default: `"float"`) | Display mode for the chat window | -| **agentId** | `string` | optional | Specific AI agent to use | -| **context** | `Record` | optional | Contextual data to pass to the AI | -| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | - -### Nested Shape: `AIChatWindowProps.aria` - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **ariaLabel** | `string \| Record` | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. | -| **ariaDescribedBy** | `string` | optional | ID of element providing additional description (WAI-ARIA aria-describedby) | -| **role** | `string` | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") | - - --- ## ActionButtonProps diff --git a/content/docs/references/ui/page.mdx b/content/docs/references/ui/page.mdx index 02305770f86..a34314aa73b 100644 --- a/content/docs/references/ui/page.mdx +++ b/content/docs/references/ui/page.mdx @@ -246,7 +246,7 @@ View filter rule | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **type** | `Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| 'page:card' \| 'page:section' \| 'record:details' \| 'record:highlights' \| 'record:related_list' \| 'record:activity' \| 'record:chatter' \| 'record:discussion' \| 'record:path' \| 'record:alert' \| 'record:quick_actions' \| 'record:reference_rail' \| 'record:history' \| 'app:launcher' \| 'nav:menu' \| 'nav:breadcrumb' \| 'global:search' \| 'global:notifications' \| 'ai:chat_window' \| 'ai:suggestion' \| 'element:text' \| 'element:number' \| 'element:image' \| 'element:divider' \| 'element:button' \| 'element:record_picker' \| 'element:text_input'> \| string` | ✅ | Component Type — a standard vocabulary member, or a custom/registered component type in its own namespace (e.g. `object-grid`, `mcp:connect-agent`). The spec's own type namespaces are a closed vocabulary at author time: inside them, a type the vocabulary does not declare is refused by `os validate` / `os build` / `os lint` (rule `component-type-unknown`); a type the vocabulary RETIRED by name (`user:profile` — shell chrome, not author-placeable; `element:filter` and `element:form` — retired whole, no renderer for either ever shipped) is refused at the parse itself, with the retirement prescription. | +| **type** | `Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| 'page:card' \| 'page:section' \| 'record:details' \| 'record:highlights' \| 'record:related_list' \| 'record:activity' \| 'record:chatter' \| 'record:discussion' \| 'record:path' \| 'record:alert' \| 'record:quick_actions' \| 'record:reference_rail' \| 'record:history' \| 'app:launcher' \| 'nav:menu' \| 'nav:breadcrumb' \| 'global:search' \| 'global:notifications' \| 'ai:suggestion' \| 'element:text' \| 'element:number' \| 'element:image' \| 'element:divider' \| 'element:button' \| 'element:record_picker' \| 'element:text_input'> \| string` | ✅ | Component Type — a standard vocabulary member, or a custom/registered component type in its own namespace (e.g. `object-grid`, `mcp:connect-agent`). The spec's own type namespaces are a closed vocabulary at author time: inside them, a type the vocabulary does not declare is refused by `os validate` / `os build` / `os lint` (rule `component-type-unknown`); a type the vocabulary RETIRED by name (`user:profile` — shell chrome, not author-placeable; `ai:chat_window` — no renderer by design, the floating chat overlay is the AI chat entry point; `element:filter` and `element:form` — retired whole, no renderer for either ever shipped) is refused at the parse itself, with the retirement prescription. | | **id** | `string` | optional | Unique instance ID | | **label** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | | **properties** | `Record` | optional (default: `{}`) | Component props passed to the widget. See component.zod.ts for schemas. | @@ -317,7 +317,6 @@ View filter rule * `nav:breadcrumb` * `global:search` * `global:notifications` -* `ai:chat_window` * `ai:suggestion` * `element:text` * `element:number` @@ -344,7 +343,7 @@ View filter rule | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **type** | `Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| …> \| string` | ✅ | Component Type — a standard vocabulary member, or a custom/registered component type in its own namespace (e.g. `object-grid`, `mcp:connect-agent`). The spec's own type namespaces are a closed vocabulary at author time: inside them, a type the vocabulary does not declare is refused by `os validate` / `os build` / `os lint` (rule `component-type-unknown`); a type the vocabulary RETIRED by name (`user:profile` — shell chrome, not author-placeable; `element:filter` and `element:form` — retired whole, no renderer for either ever shipped) is refused at the parse itself, with the retirement prescription. | +| **type** | `Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| …> \| string` | ✅ | Component Type — a standard vocabulary member, or a custom/registered component type in its own namespace (e.g. `object-grid`, `mcp:connect-agent`). The spec's own type namespaces are a closed vocabulary at author time: inside them, a type the vocabulary does not declare is refused by `os validate` / `os build` / `os lint` (rule `component-type-unknown`); a type the vocabulary RETIRED by name (`user:profile` — shell chrome, not author-placeable; `ai:chat_window` — no renderer by design, the floating chat overlay is the AI chat entry point; `element:filter` and `element:form` — retired whole, no renderer for either ever shipped) is refused at the parse itself, with the retirement prescription. | | **id** | `string` | optional | Unique instance ID | | **label** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | | **properties** | `Record` | optional (default: `{}`) | Component props passed to the widget. See component.zod.ts for schemas. | diff --git a/content/docs/ui/pages.mdx b/content/docs/ui/pages.mdx index 3ec9cb5ac91..01564f20b16 100644 --- a/content/docs/ui/pages.mdx +++ b/content/docs/ui/pages.mdx @@ -183,7 +183,7 @@ The `type` field is a union of the standard `PageComponentType` enum and any cus - **Record context:** `record:details`, `record:highlights`, `record:related_list`, `record:activity`, `record:chatter`, `record:path`, `record:alert`, `record:quick_actions`, `record:reference_rail`, `record:history` — each renders from the record context a **record page** mounts, so they belong on a `type:'record'` page. A `kind:'react'` page mounts no such context and `os validate` rejects them there (see Validating metadata §10b) - **Navigation:** `app:launcher`, `nav:menu`, `nav:breadcrumb` - **Utility:** `global:search`, `global:notifications` — `user:profile` is **not** author-placeable: it is shell chrome (the signed-in user's avatar menu, which the app shell renders itself on every page), no renderer exists for it by ruling (objectstack#14159 / objectui#7135), and an authored `user:profile` node is refused at the schema door — `definePage()`, `os validate`, `os build` — with that prescription at the node's path instead of drawing an unknown-type panel in front of a user -- **AI:** `ai:chat_window`, `ai:suggestion` +- **AI:** `ai:suggestion` — `ai:chat_window` was retired in v17.x: AI chat is not a page element, and the floating chat overlay the console mounts on every page is the supported entry point (set the app's `defaultAgent` to choose which platform agent answers there). No renderer for it ever shipped, and an authored `ai:chat_window` node is refused at the schema door — `definePage()`, `os validate`, `os build` — with that prescription at the node's path instead of drawing an unknown-type panel in front of a user - **Elements:** `element:text`, `element:number`, `element:image`, `element:divider`, `element:button`, `element:record_picker`, `element:text_input` (`element:filter` and `element:form` were retired in v17.x — no renderer ever shipped for either, and an authored node of either type is refused at the schema door — `definePage()`, `os validate`, `os build` — with that prescription at the node's path, bare node included. List surfaces own their filtering via a view's `userFilters` quick-filter bar or the list toolbar's filter builder; for forms use the object-bound `object-form` block, which is rendered and designer-publishable) Components may also carry `dataSource` (per-element object binding for multi-object pages), `responsiveStyles` (per-breakpoint scoped CSS, ADR-0065), and `aria` configuration. Custom string types are also accepted for project-specific widgets. (The former `responsive` layout block was retired in v17.x — no renderer ever applied it; see the upgrade guide.) diff --git a/docs/audits/2026-07-unknown-key-strictness-ledger.counts/ui.md b/docs/audits/2026-07-unknown-key-strictness-ledger.counts/ui.md index 0846a3167bd..71224ae481c 100644 --- a/docs/audits/2026-07-unknown-key-strictness-ledger.counts/ui.md +++ b/docs/audits/2026-07-unknown-key-strictness-ledger.counts/ui.md @@ -21,7 +21,7 @@ The `strict` column is the one the campaign schedules against; it counts both th | Dir | Sites | strict | passthrough | catchall | strip | |---|---|---|---|---|---| -| `ui/` | 190 | 180 | 3 | 0 | 7 | +| `ui/` | 189 | 179 | 3 | 0 | 7 | ## `ui/` — sites @@ -36,7 +36,7 @@ classify and is not listed (it becomes reportable the day it grows its first sit | `app.zod.ts` | 19 | | `bulk-action.zod.ts` | 4 | | `chart.zod.ts` | 8 | -| `component.zod.ts` | 60 | +| `component.zod.ts` | 59 | | `dashboard.zod.ts` | 11 | | `dataset.zod.ts` | 4 | | `i18n.zod.ts` | 1 | @@ -46,7 +46,7 @@ classify and is not listed (it becomes reportable the day it grows its first sit | `sharing.zod.ts` | 1 | | `view.zod.ts` | 60 | | `widget.zod.ts` | 1 | -| **total** | **190** | +| **total** | **189** | ## `ui/` — open @@ -54,7 +54,7 @@ Per file, how many of its sites still silently discard unknown keys. The `Class` column that decides the bucket split is hand-written in the ledger; the arithmetic over it is here. -**7 strip of 190**, in 4 file(s). +**7 strip of 189**, in 4 file(s). | File | Strip | Sites | |---|---|---| @@ -62,7 +62,7 @@ over it is here. | `app.zod.ts` | 1 | 19 | | `view.zod.ts` | 4 | 60 | | `widget.zod.ts` | 1 | 1 | -| **total** | **7** | **190** | +| **total** | **7** | **189** | | Bucket | Sites | |---|---| diff --git a/packages/lint/src/validate-page-field-bindings.ts b/packages/lint/src/validate-page-field-bindings.ts index 040e1642826..ad7b4cc0fb3 100644 --- a/packages/lint/src/validate-page-field-bindings.ts +++ b/packages/lint/src/validate-page-field-bindings.ts @@ -26,7 +26,7 @@ * * `ComponentPropsMap` cannot drive this rule: a Zod schema does not say which * of its `z.string()` props is a FIELD NAME (`RecordPathProps.statusField` and - * `AIChatWindowProps.agentId` are both plain strings), and the type universe is + * `ElementImagePropsSchema.alt` are both plain strings), and the type universe is * open anyway — `PageComponent.type` is `z.union([PageComponentType, * z.string()])`, so unregistered types like `record:line_items` parse and are * authored in the wild. The table below names the field-bearing props diff --git a/packages/spec/api-surface/ui.json b/packages/spec/api-surface/ui.json index b1663817a1f..1bfd8e7e731 100644 --- a/packages/spec/api-surface/ui.json +++ b/packages/spec/api-surface/ui.json @@ -4,7 +4,6 @@ "exports": [ "ACTION_LOCATIONS (const)", "ACTION_PARAM_BUILTIN_KEYS (const)", - "AIChatWindowProps (const)", "ASSEMBLED_VIEW_ITEMS_KEY (const)", "Action (const)", "Action (type)", diff --git a/packages/spec/authorable-defaults/ui.json b/packages/spec/authorable-defaults/ui.json index 6edaf610e42..9cd6a41865a 100644 --- a/packages/spec/authorable-defaults/ui.json +++ b/packages/spec/authorable-defaults/ui.json @@ -2,7 +2,6 @@ "description": "Ratchet of the DEFAULT VALUE of every authorable key in one category that has one (#4666) — what a metadata author gets when they omit the key, which for AI-authored metadata is most of the time. Sharded by category like authorable-surface/; the gate reads the whole authorable-defaults/ directory as ONE set. Each line is \": = \". Additions (a NEW key that ships with a default) are auto-recorded — commit the change. CHANGING, ADDING or REMOVING the default of a key that already existed is NOT auto-recorded: it silently alters the behaviour of already-deployed metadata, so it fails check:authorable-surface until it is declared in DEFAULT_CHANGES_BY_MAJOR (scripts/lib/default-changes.ts). Constraints are deliberately NOT recorded here — a tightened bound REJECTS a document loudly, which is a different and self-announcing class (maintainer ruling on #4666, direction B). See #4666, #4661.", "category": "ui", "defaults": [ - "ui/AIChatWindowProps:mode = \"float\"", "ui/Action:refreshAfter = false", "ui/Action:type = \"script\"", "ui/ActionAi:exposed = false", diff --git a/packages/spec/authorable-surface/ui.json b/packages/spec/authorable-surface/ui.json index bcee998a338..43f54526a8a 100644 --- a/packages/spec/authorable-surface/ui.json +++ b/packages/spec/authorable-surface/ui.json @@ -2,10 +2,6 @@ "description": "Ratchet of every AUTHORABLE key in one category of the spec — what a metadata author may write, which for this platform IS the third-party API. Sharded by category (#5837) so two PRs touching different categories never share a file; the gate reads the whole authorable-surface/ directory as ONE set, so deleting a shard deletes its keys exactly as deleting lines did. Auto-updated on additions (commit the change). A key that disappears without a tombstone fails gen:schema, because these schemas are not .strict() and Zod would silently strip it. \"[RETIRED]\" marks a tombstoned key that still rejects with an upgrade prescription. See #3855, ADR-0059 §5.", "category": "ui", "keys": [ - "ui/AIChatWindowProps:agentId", - "ui/AIChatWindowProps:aria", - "ui/AIChatWindowProps:context", - "ui/AIChatWindowProps:mode", "ui/Action:_lock", "ui/Action:_lockDocsUrl", "ui/Action:_lockReason", diff --git a/packages/spec/declaration-map/ui.json b/packages/spec/declaration-map/ui.json index 5239f6a5a3a..d22e57eedb4 100644 --- a/packages/spec/declaration-map/ui.json +++ b/packages/spec/declaration-map/ui.json @@ -2,7 +2,6 @@ "description": "TS declaration name → spec registry name (def key) for the schemas @objectstack/spec publishes: entries['ObjectSchemaBase'] === 'data/Object'. Composed from json-schema.manifest/ (the def keys), export-origins/ (which declaration each export resolves to), and a syntactic unwinding of wrapper initializers that recovers module-private base declarations (the ObjectSchemaBase case — see scripts/build-declaration-map.ts). A name that maps to two different def keys is DROPPED into `collisions` rather than guessed, so a lookup miss means \"not known to be an authorable container\". Consumers hold a changed line’s enclosing declaration name and ask which authorable container it declares (docs-audit anchor qualification is the funding one). Generated — never hand-edited; regenerate with `pnpm --filter @objectstack/spec gen:declaration-map` and read the diff.", "category": "ui", "entries": { - "AIChatWindowProps": "ui/AIChatWindowProps", "Action": "ui/Action", "ActionAi": "ui/ActionAi", "ActionAiSchema": "ui/ActionAi", diff --git a/packages/spec/docs-import-surface.baseline.json b/packages/spec/docs-import-surface.baseline.json index 6a43cacefc2..89dd3292fcd 100644 --- a/packages/spec/docs-import-surface.baseline.json +++ b/packages/spec/docs-import-surface.baseline.json @@ -41,7 +41,6 @@ "system/AddFieldOperation — no type export", "system/CreateObjectOperation — no type export", "system/DeployStatusEnum — no type export", - "ui/AIChatWindowProps — no type export", "ui/DerivedMeasureOp — no type export", "ui/ElementButtonProps — no type export", "ui/ElementFilterProps — no type export", diff --git a/packages/spec/export-origins/ui.json b/packages/spec/export-origins/ui.json index b1e99c14ff2..fc8d29646b2 100644 --- a/packages/spec/export-origins/ui.json +++ b/packages/spec/export-origins/ui.json @@ -4,7 +4,6 @@ "exports": { "ACTION_LOCATIONS": "src/ui/action.zod.ts#ACTION_LOCATIONS (const)", "ACTION_PARAM_BUILTIN_KEYS": "src/ui/action-params.zod.ts#ACTION_PARAM_BUILTIN_KEYS (const)", - "AIChatWindowProps": "src/ui/component.zod.ts#AIChatWindowProps (const)", "ASSEMBLED_VIEW_ITEMS_KEY": "src/ui/assembled-views.zod.ts#ASSEMBLED_VIEW_ITEMS_KEY (const)", "Action": "src/ui/action.zod.ts#Action (type)", "ActionAi": "src/ui/action.zod.ts#ActionAi (type)", diff --git a/packages/spec/json-schema.manifest/ui.json b/packages/spec/json-schema.manifest/ui.json index 42eed1b6b4a..57c426c4c5c 100644 --- a/packages/spec/json-schema.manifest/ui.json +++ b/packages/spec/json-schema.manifest/ui.json @@ -2,7 +2,6 @@ "description": "Ratchet manifest of every JSON Schema emitted by scripts/build-schemas.ts for one category. Sharded by category (#5837); the gate reads the whole json-schema.manifest/ directory as ONE set. Auto-appended when new schemas are added (commit the change). A listed schema that a build no longer emits fails gen:schema. DELETING a key is gated too (#4725): the removal is measured against this directory at the merge base with origin/main — which the commit under test cannot rewrite — and every def that leaves the published set must be declared in RETIRED_DEFS_BY_MAJOR (src/migrations/registry.ts), or in RENAMED_DEFS (scripts/lib/renamed-defs.ts) when it is a rename rather than a removal. See #2978, #4725.", "category": "ui", "schemas": [ - "ui/AIChatWindowProps", "ui/Action", "ui/ActionAi", "ui/ActionButtonProps", diff --git a/packages/spec/src/migrations/entries/retired-defs/18.ui__AIChatWindowProps.ts b/packages/spec/src/migrations/entries/retired-defs/18.ui__AIChatWindowProps.ts new file mode 100644 index 00000000000..0fea15770f9 --- /dev/null +++ b/packages/spec/src/migrations/entries/retired-defs/18.ui__AIChatWindowProps.ts @@ -0,0 +1,17 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +// #21504 — `ui/AIChatWindowProps` (`mode`, `agentId`, `context`, `aria`) leaves +// with the `ai:chat_window` page element it described (ADR-0049 +// enforce-or-remove; triage ruling 5963897014, the `user:profile` precedent of +// #14159). No renderer for the element ever shipped, so none of the four keys +// was read anywhere. Its only carrier, the `ComponentPropsMap['ai:chat_window']` +// row, is kept as a whole-bag refusal (`retiredComponentProps`) that answers +// with the element's retirement prescription, and the node itself is refused by +// name at `PageComponentSchema.type`. Upgraders get the D3 semantic entry +// `ui-ai-chat-window-retired`. +// +// Registered under 18, not 17: v17.0.0 was cut before this landed, so the +// removal ships on the 17.x line (launch-window convention: accept-set +// narrowings ride minor releases) and the prescription lives at the major +// boundary where `migrate meta` users look. +export const entry = 'ui/AIChatWindowProps'; diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-ai-chat-window-retired.ts b/packages/spec/src/migrations/entries/semantic/18.ui-ai-chat-window-retired.ts new file mode 100644 index 00000000000..01e826920a9 --- /dev/null +++ b/packages/spec/src/migrations/entries/semantic/18.ui-ai-chat-window-retired.ts @@ -0,0 +1,51 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import type { SemanticMigration } from '../../types.js'; + +// #21504 — the D3 record of the `ai:chat_window` retirement (ADR-0049 +// enforce-or-remove; triage ruling 5963897014: retire, refused BY NAME through +// `RETIRED_PAGE_COMPONENT_TYPES`, the `user:profile` precedent of #14159). There +// is no D2 half: the element's four keys left with its props def, so there is no +// key a walker could strip and leave a valid node behind, and deleting an +// authored page node is a layout decision a mechanical conversion must not make +// (the `element-filter-removed` docblock's rule). What the chain owes the +// upgrader is the delegation itself, in writing — the `element:filter` / +// `element:form` node entry's shape, for a node the parse now refuses by name. +// +// The replacement is not new prose: it is the node-level refusal message in +// `RETIRED_PAGE_COMPONENT_TYPES` (ui/page.zod.ts), so the door that refuses and +// the chain that prescribes carry one instruction. +export const entry: SemanticMigration = { + id: 'ui-ai-chat-window-retired', + surface: + 'page.component.ai:chat_window — the component node, with every key its props bag ' + + 'declared (`mode`, `agentId`, `context`, `aria`), in regions, named slots and nested ' + + 'containers alike', + replacement: + 'Delete the component node and put nothing in its place: AI chat is not a page element, ' + + 'and the floating chat overlay the console mounts on every page is the supported entry ' + + 'point. To choose which platform agent the overlay answers with — what `agentId` reached ' + + 'for — set the app\'s `defaultAgent` (a platform agent: `ask`, or `build` on an authoring ' + + 'surface). `mode`, `context` and `aria` have no counterpart on the page: none of them was ' + + 'ever read, and the overlay is not configured per page', + reason: + 'No renderer for `ai:chat_window` ever shipped in objectui, framework or cloud, and none is ' + + 'wanted: the console leaves it unregistered on purpose so that a page naming it fails ' + + 'loudly, and Studio\'s page palette excludes it. So the element and its four keys were a ' + + 'capability claim nothing kept — a page that placed one validated clean and drew "Unknown ' + + 'component type" in front of an end user. Zero producers were measured in objectstack, ' + + 'cloud and hotcrm (one comment naming it as dropped). The name is now refused at ' + + '`PageComponentSchema.type`, its `ComponentPropsMap` row refuses every props bag with the ' + + 'same prescription, and the enum no longer lists it; `ai:suggestion` is unchanged. No ' + + 'conversion is registered, because the only edit is deleting the node, and which region ' + + 'closes up, holds something else, or keeps its slot is the author\'s judgment about a ' + + 'page they composed — this entry is that delegation', + acceptanceCriteria: + 'No `ai:chat_window` component remains in any page — regions, named slots and nested ' + + 'containers alike. `os validate` is clean: a remaining node is reported at its own `type` ' + + 'path with `params.retiredComponentType` naming `ai:chat_window`, so each one is named ' + + 'individually rather than as one page-level failure. An app whose removed node named an ' + + '`agentId` now names that platform agent in its `defaultAgent` instead, or leaves it unset ' + + 'for the default `ask`. Replaying the 17 → 18 chain over the edited source then reports the ' + + 'migrated stack schema-valid — `schemaValid: true` in `--json`', +}; diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index 3641f88540d..3233239ec55 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -6083,6 +6083,21 @@ const STEP18_RATIONALE: readonly RationaleFragment[] = [ + 'authors are refused at parse; its D3 record is the semantic entry ' + '`translation-widget-sub-caption-retired`.', }, + { + id: 'ui-ai-chat-window-retired', + order: 65, + text: + 'It also retires the `ai:chat_window` page element (#21504, ADR-0049 enforce-or-remove), ' + + 'the `user:profile` shape one namespace over: no renderer for it ever shipped, and none is ' + + 'wanted — the console leaves it unregistered on purpose, because the floating chat overlay ' + + 'it mounts on every page is the supported AI chat entry point — so a page that placed one ' + + 'validated clean and drew "Unknown component type", and its four props configured nothing. ' + + 'The name leaves `PageComponentType` and is refused by name at the node, its ' + + '`ComponentPropsMap` row stays as a whole-bag refusal carrying the same prescription, and ' + + 'the props def `AIChatWindowProps` is unpublished. No conversion is registered: the only ' + + 'edit is deleting the node, a layout decision that is the author\'s. Its D3 record is the ' + + 'semantic entry `ui-ai-chat-window-retired`; `ai:suggestion` is unchanged.', + }, { id: 'ui-form-layout-inline-grid-retired', order: 40, @@ -18799,6 +18814,53 @@ const step18: MigrationStep = { + 'both fulfilling shapes and the runtime that fulfils each; the author adds the shape ' + 'they meant or drops the flag.', }, + // #21504 — the D3 record of the `ai:chat_window` retirement (ADR-0049 + // enforce-or-remove; triage ruling 5963897014: retire, refused BY NAME through + // `RETIRED_PAGE_COMPONENT_TYPES`, the `user:profile` precedent of #14159). There + // is no D2 half: the element's four keys left with its props def, so there is no + // key a walker could strip and leave a valid node behind, and deleting an + // authored page node is a layout decision a mechanical conversion must not make + // (the `element-filter-removed` docblock's rule). What the chain owes the + // upgrader is the delegation itself, in writing — the `element:filter` / + // `element:form` node entry's shape, for a node the parse now refuses by name. + // + // The replacement is not new prose: it is the node-level refusal message in + // `RETIRED_PAGE_COMPONENT_TYPES` (ui/page.zod.ts), so the door that refuses and + // the chain that prescribes carry one instruction. + { + id: 'ui-ai-chat-window-retired', + surface: + 'page.component.ai:chat_window — the component node, with every key its props bag ' + + 'declared (`mode`, `agentId`, `context`, `aria`), in regions, named slots and nested ' + + 'containers alike', + replacement: + 'Delete the component node and put nothing in its place: AI chat is not a page element, ' + + 'and the floating chat overlay the console mounts on every page is the supported entry ' + + 'point. To choose which platform agent the overlay answers with — what `agentId` reached ' + + 'for — set the app\'s `defaultAgent` (a platform agent: `ask`, or `build` on an authoring ' + + 'surface). `mode`, `context` and `aria` have no counterpart on the page: none of them was ' + + 'ever read, and the overlay is not configured per page', + reason: + 'No renderer for `ai:chat_window` ever shipped in objectui, framework or cloud, and none is ' + + 'wanted: the console leaves it unregistered on purpose so that a page naming it fails ' + + 'loudly, and Studio\'s page palette excludes it. So the element and its four keys were a ' + + 'capability claim nothing kept — a page that placed one validated clean and drew "Unknown ' + + 'component type" in front of an end user. Zero producers were measured in objectstack, ' + + 'cloud and hotcrm (one comment naming it as dropped). The name is now refused at ' + + '`PageComponentSchema.type`, its `ComponentPropsMap` row refuses every props bag with the ' + + 'same prescription, and the enum no longer lists it; `ai:suggestion` is unchanged. No ' + + 'conversion is registered, because the only edit is deleting the node, and which region ' + + 'closes up, holds something else, or keeps its slot is the author\'s judgment about a ' + + 'page they composed — this entry is that delegation', + acceptanceCriteria: + 'No `ai:chat_window` component remains in any page — regions, named slots and nested ' + + 'containers alike. `os validate` is clean: a remaining node is reported at its own `type` ' + + 'path with `params.retiredComponentType` naming `ai:chat_window`, so each one is named ' + + 'individually rather than as one page-level failure. An app whose removed node named an ' + + '`agentId` now names that platform agent in its `defaultAgent` instead, or leaves it unset ' + + 'for the default `ask`. Replaying the 17 → 18 chain over the edited source then reports the ' + + 'migrated stack schema-valid — `schemaValid: true` in `--json`', + }, // The one key this close DECLARES rather than refuses is `dependsOn`, so an author // who wrote it keeps working and now has a contract saying so. Everything else // undeclared becomes a parse error. Registered as a structured TODO (ADR-0087 D3) @@ -26692,6 +26754,21 @@ export const RETIRED_DEFS_BY_MAJOR: Readonly> // entries stay as history — gate (b2) of build-schemas.ts accepts an entry // naming a key the build no longer emits. 'system/TrainingRecord', + // #21504 — `ui/AIChatWindowProps` (`mode`, `agentId`, `context`, `aria`) leaves + // with the `ai:chat_window` page element it described (ADR-0049 + // enforce-or-remove; triage ruling 5963897014, the `user:profile` precedent of + // #14159). No renderer for the element ever shipped, so none of the four keys + // was read anywhere. Its only carrier, the `ComponentPropsMap['ai:chat_window']` + // row, is kept as a whole-bag refusal (`retiredComponentProps`) that answers + // with the element's retirement prescription, and the node itself is refused by + // name at `PageComponentSchema.type`. Upgraders get the D3 semantic entry + // `ui-ai-chat-window-retired`. + // + // Registered under 18, not 17: v17.0.0 was cut before this landed, so the + // removal ships on the 17.x line (launch-window convention: accept-set + // narrowings ride minor releases) and the prescription lives at the major + // boundary where `migrate meta` users look. + 'ui/AIChatWindowProps', // Commit 35ad101bc — `ui/BorderRadius` (the border-radius scale sub-block) left with `ui/Theme`: // its ONLY consumer was the retired `ThemeSchema` (the #3950 rule — an // exported value schema with no consumer reads as a capability). See diff --git a/packages/spec/src/ui/component-props-unknown-members.pin.test.ts b/packages/spec/src/ui/component-props-unknown-members.pin.test.ts index 03b1bf90ece..80e8d85afdf 100644 --- a/packages/spec/src/ui/component-props-unknown-members.pin.test.ts +++ b/packages/spec/src/ui/component-props-unknown-members.pin.test.ts @@ -214,10 +214,6 @@ on(['object-grid'], ['pagination.*'], { kind: 'open-bag', why: '`z.looseObject` on purpose: `pageSize` and `pageSizeOptions` are typed and are the only members a read point names; the member\'s own docblock records why the bag stays open', }); -on(['ai:chat_window'], ['context{}'], { - kind: 'no-reader', - why: 'objectui registers no `ai:chat_window` renderer (`components/src/renderers/placeholders.tsx:109-113`), so nothing reads this row at all; whether the row retires is its own question', -}); // Read with a fixed shape at the pin — the later stages. on(['object-metric'], ['aggregate'], staged('object-metric', 'plugin-dashboard/src/ObjectMetricWidget.tsx:250, `.field` / `.function` / `.groupBy` at :404-443')); diff --git a/packages/spec/src/ui/component-type-vocabulary.test.ts b/packages/spec/src/ui/component-type-vocabulary.test.ts index 53247b9e0ef..8342f9267be 100644 --- a/packages/spec/src/ui/component-type-vocabulary.test.ts +++ b/packages/spec/src/ui/component-type-vocabulary.test.ts @@ -126,6 +126,23 @@ describe('KNOWN_COMPONENT_TYPES covers every declared face', () => { expect(hasReservedComponentNamespace('user:profile')).toBe(false); expect(isKnownComponentType('user:profile')).toBe(true); }); + + /** + * #21504 — `ai:chat_window` is retired BY NAME the `user:profile` way: out + * of the enum, KNOWN through its kept `ComponentPropsMap` row, and out of the + * typo candidates so no suggester renames an author into it. Unlike + * `user:profile`, its namespace STAYS reserved — `ai:suggestion` still + * populates `ai:` — so the namespace list above is unchanged and the + * `component-type-unknown` rule keeps claiming `ai:`. + */ + it('ai:chat_window is known through its kept row, not a candidate, and `ai:` stays reserved (#21504)', () => { + expect(PageComponentType.options).not.toContain('ai:chat_window'); + expect(isKnownComponentType('ai:chat_window')).toBe(true); + expect(KNOWN_COMPONENT_TYPE_CANDIDATES).not.toContain('ai:chat_window'); + expect(hasReservedComponentNamespace('ai:chat_window')).toBe(true); + // Lit control: the kept member of the same namespace is a candidate. + expect(KNOWN_COMPONENT_TYPE_CANDIDATES).toContain('ai:suggestion'); + }); }); describe('STRING_ARM_REGISTERED_TYPES ledger discipline', () => { diff --git a/packages/spec/src/ui/component.test.ts b/packages/spec/src/ui/component.test.ts index 389e297876c..b7704d84dca 100644 --- a/packages/spec/src/ui/component.test.ts +++ b/packages/spec/src/ui/component.test.ts @@ -1287,14 +1287,15 @@ describe('ComponentPropsMap', () => { }); it('should contain AI components', () => { + // `ai:chat_window`'s row is KEPT as a refusal door (#21504) — see the + // describe below; it no longer parses any bag. expect(ComponentPropsMap['ai:chat_window']).toBeDefined(); expect(ComponentPropsMap['ai:suggestion']).toBeDefined(); }); - it('should parse ai:chat_window with default', () => { - const result = ComponentPropsMap['ai:chat_window'].parse({}); - expect(result.mode).toBe('float'); - }); + // `should parse ai:chat_window with default` LEFT at #21504 — the row + // refuses every bag now; its flipped twin is the first pin of the + // `ai:chat_window is retired` describe below. it('should parse ai:suggestion with optional context', () => { const result = ComponentPropsMap['ai:suggestion'].parse({}); @@ -1443,6 +1444,119 @@ describe('ComponentPropsMap', () => { }); }); + // #21504 — triage ruling 5963897014: retire `ai:chat_window` under ADR-0049 + // enforce-or-remove, refused BY NAME, the `user:profile` precedent above. + // Zero producers measured, and objectui registers no renderer on purpose — + // the console's floating chat overlay is the AI chat entry point. Same pin + // shape as that describe: `code` + `path` + `params` + the prescription's + // first sentence at each door, never a bare `toThrow()`, which greens on any + // error. The control is `ai:suggestion`, the member of the same namespace the + // ruling keeps (it has a placeholder renderer — a different class). + describe('ai:chat_window is retired, refused by name (#21504)', () => { + const FIRST_SENTENCE = + /^`ai:chat_window` was removed in @objectstack\/spec 17 \(ADR-0049\) — no renderer for it ever shipped: the console leaves it unregistered on purpose, so a page that placed one validated clean and then drew "Unknown component type" in front of an end user, and its `mode`, `agentId`, `context` and `aria` props configured nothing\./; + // The supported entry point is NAMED, and the fix is imperative. + const ENTRY_POINT = 'the floating chat overlay the console mounts on every page is the supported entry point'; + const FIX = 'Delete the `ai:chat_window` component node'; + // `check:doc-authoring` (maintainer ruling 2026-08-12): a prescription + // printed at the customer carries no citation-shaped issue id. + const ISSUE_ID = /#\d{3,}/; + + it('the map carries the prescription: the overlay named, the fix imperative, no tracker number', () => { + const guidance = RETIRED_PAGE_COMPONENT_TYPES.get('ai:chat_window'); + expect(guidance).toBeTypeOf('string'); + expect(guidance!).toMatch(FIRST_SENTENCE); + expect(guidance!).toContain(ENTRY_POINT); + expect(guidance!).toContain(FIX); + expect(guidance!).toContain('ADR-0049'); + expect(guidance!).not.toMatch(ISSUE_ID); + }); + + it('the ComponentPropsMap row refuses even the empty bag — the flipped accept pin', () => { + for (const bag of [{}, { mode: 'float' }, { agentId: 'ask', context: { recordId: 'r1' } }]) { + const r = ComponentPropsMap['ai:chat_window'].safeParse(bag); + expect(r.success, JSON.stringify(bag)).toBe(false); + if (r.success) continue; + expect(r.error.issues).toHaveLength(1); + const issue = r.error.issues[0]!; + // The `retiredKey` channel at element grain: `expected: 'never'`. + expect(issue.code).toBe('invalid_type'); + expect(issue.path).toEqual([]); + expect(issue.message).toBe(RETIRED_PAGE_COMPONENT_TYPES.get('ai:chat_window')); + } + }); + + it('PageComponentSchema refuses the node by name at `type`, bare or populated', () => { + for (const properties of [undefined, {}, { mode: 'inline' }]) { + const r = PageComponentSchema.safeParse( + properties === undefined ? { type: 'ai:chat_window' } : { type: 'ai:chat_window', properties }, + ); + expect(r.success, `properties=${JSON.stringify(properties)}`).toBe(false); + if (r.success) continue; + expect(r.error.issues).toHaveLength(1); + const issue = r.error.issues[0]!; + expect(issue.code).toBe('custom'); + expect(issue.path).toEqual(['type']); + expect(issue.message).toMatch(FIRST_SENTENCE); + expect((issue as { params?: Record }).params).toEqual({ retiredComponentType: 'ai:chat_window' }); + } + }); + + it('PageSchema refuses an authored page at the element path — the door `os validate` parses', () => { + const r = PageSchema.safeParse({ + name: 'account_detail', + label: 'Account', + regions: [{ + name: 'main', + components: [ + { type: 'page:header', properties: { title: 'Account' } }, + { type: 'ai:chat_window', properties: { mode: 'sidebar' } }, + ], + }], + }); + expect(r.success).toBe(false); + if (r.success) return; + const located = r.error.issues.filter((i) => i.code === 'custom'); + expect(located).toHaveLength(1); + expect(located[0]!.path).toEqual(['regions', 0, 'components', 1, 'type']); + expect(located[0]!.message).toMatch(FIRST_SENTENCE); + }); + + it('PageComponentType itself refuses the member with the prescription — the enum error map', () => { + expect(PageComponentType.options).not.toContain('ai:chat_window'); + const r = PageComponentType.safeParse('ai:chat_window'); + expect(r.success).toBe(false); + if (r.success) return; + expect(r.error.issues[0]!.code).toBe('invalid_value'); + expect(r.error.issues[0]!.message).toMatch(FIRST_SENTENCE); + // Only a value that USED to be legal gets the prescription — a stranger + // keeps zod's own enum message. + const stranger = PageComponentType.safeParse('ai:chat'); + expect(stranger.success).toBe(false); + if (!stranger.success) expect(stranger.error.issues[0]!.message).not.toContain('floating chat overlay'); + }); + + it('control: `ai:suggestion`, the member the ruling keeps, still parses at every door', () => { + expect(RETIRED_PAGE_COMPONENT_TYPES.has('ai:suggestion')).toBe(false); + expect(PageComponentType.options).toContain('ai:suggestion'); + expect(PageComponentType.safeParse('ai:suggestion').success).toBe(true); + expect(ComponentPropsMap['ai:suggestion'].safeParse({ context: 'account' }).success).toBe(true); + expect(PageComponentSchema.safeParse({ type: 'ai:suggestion', properties: { context: 'account' } }).success).toBe(true); + const page = PageSchema.safeParse({ + name: 'account_detail', + label: 'Account', + regions: [{ name: 'main', components: [{ type: 'ai:suggestion' }] }], + }); + expect(page.success).toBe(true); + }); + + it('the open string arm stays open — only the retired NAME is refused', () => { + for (const type of ['ai:assistant', 'custom.chat_window', 'object-grid']) { + expect(PageComponentSchema.safeParse({ type }).success, type).toBe(true); + } + }); + }); + // #11575 — the two `@objectstack/cloud-connection` console widgets. Rows // exist so the #5068 gate's dispatch reaches them; the accepted key set is // EMPTY, measured from the renderers' read points at the `.objectui-sha` diff --git a/packages/spec/src/ui/component.zod.ts b/packages/spec/src/ui/component.zod.ts index 58b6e27fbce..891d947a323 100644 --- a/packages/spec/src/ui/component.zod.ts +++ b/packages/spec/src/ui/component.zod.ts @@ -404,7 +404,8 @@ const emptyProps = (type: string) => /** * A component RETIRED at element grain whose props bag is refused WHOLE — - * `user:profile` (#14159). The `retiredKey` channel one grain wider: where a + * `user:profile` (#14159), and `ai:chat_window` (#21504), whose four keys left + * with its props def. The `retiredKey` channel one grain wider: where a * tombstoned KEY accepts absence and refuses any value, a retired ELEMENT has * nothing an author may write at all, so the row is `z.never` — `{}` is refused * exactly like a populated bag, `expected: 'never'` / `code: 'invalid_type'` is @@ -2354,17 +2355,12 @@ export const PageAccordionProps = strictObject({ aria: AriaPropsSchema.optional().describe('ARIA accessibility attributes'), }); -export const AIChatWindowProps = strictObject({ - surface: 'this `ai:chat_window`', - history: PROPS_HISTORY, - guidanceSets: COMPONENT_LEVEL_GUIDANCE, -}, { - mode: z.enum(['float', 'sidebar', 'inline']).default('float').describe('Display mode for the chat window'), - agentId: z.string().optional().describe('Specific AI agent to use'), - context: z.record(z.string(), z.unknown()).optional().describe('Contextual data to pass to the AI'), - /** ARIA accessibility */ - aria: AriaPropsSchema.optional().describe('ARIA accessibility attributes'), -}); +// `AIChatWindowProps` REMOVED (#21504, ADR-0049) with the `ai:chat_window` +// element it described: `mode` / `agentId` / `context` / `aria` were read by +// nothing, because no renderer for the element ever shipped. Its row in +// `ComponentPropsMap` below now refuses the whole bag +// (`retiredComponentProps`), and the def is recorded in +// `migrations/entries/retired-defs/18.ui__AIChatWindowProps.ts`. /** * ---------------------------------------------------------------------- @@ -6700,7 +6696,16 @@ export const ComponentPropsMap = { 'mcp:connect-agent': emptyProps('mcp:connect-agent'), // AI - 'ai:chat_window': AIChatWindowProps, + // RETIRED by name (#21504, ADR-0049; triage ruling: the `user:profile` + // precedent) — no renderer by design, the console's floating chat overlay is + // the AI chat entry point. The row STAYS, for the reason `user:profile`'s + // does above: every reader that dispatches on the row keeps recognising the + // name and answers with the prescription instead of skipping it as an + // unregistered custom string. Its four keys (`mode`, `agentId`, `context`, + // `aria`) left with `AIChatWindowProps`, so the WHOLE bag is refused — the + // node itself is refused at `PageComponentSchema.type`, and a row that still + // accepted `{ mode }` would contradict that door one level up. + 'ai:chat_window': retiredComponentProps('ai:chat_window'), 'ai:suggestion': strictObject({ surface: 'this `ai:suggestion`', history: PROPS_HISTORY, diff --git a/packages/spec/src/ui/page.zod.ts b/packages/spec/src/ui/page.zod.ts index fb363a364c5..7d2faf0b1f7 100644 --- a/packages/spec/src/ui/page.zod.ts +++ b/packages/spec/src/ui/page.zod.ts @@ -118,6 +118,29 @@ export const RETIRED_PAGE_COMPONENT_TYPES: ReadonlyMap = new Map + 'kept. Delete the `element:filter` component; list surfaces own their filtering — use a ' + "view's `userFilters` quick-filter bar or the list toolbar's filter builder. " + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'], + // #21504, ADR-0049 enforce-or-remove (triage ruling 5963897014: retire, + // refused by name — the `user:profile` precedent above). Zero producers + // measured in objectstack, cloud and hotcrm, and objectui registers no + // renderer on purpose (`components/src/renderers/placeholders.tsx`, "the + // floating chat overlay (see plugin-chatbot) is the canonical entry point"), + // so an authored node validated clean and drew the loud unknown-type panel. + // The `user:profile` shape, not the `element:*` one: the row's four keys go + // with the element, so the kept `ComponentPropsMap` row refuses the WHOLE bag + // (`retiredComponentProps`) with this text — no per-key tombstones, because + // nothing about any single key is worth saying apart from "the element is + // gone". No `os migrate meta` sentence either: there is no mechanical edit to + // list (a conversion does not delete authored page nodes), and the D3 entry + // `ui-ai-chat-window-retired` carries the delegated deletion. `defaultAgent` + // is named because it is the live knob for the one thing `agentId` reached + // for: which platform agent the ambient chat answers with (ui/app.zod.ts). + ['ai:chat_window', '`ai:chat_window` was removed in @objectstack/spec 17 (ADR-0049) — no ' + + 'renderer for it ever shipped: the console leaves it unregistered on purpose, so a page ' + + 'that placed one validated clean and then drew "Unknown component type" in front of an ' + + 'end user, and its `mode`, `agentId`, `context` and `aria` props configured nothing. AI ' + + 'chat is not a page element: the floating chat overlay the console mounts on every page ' + + 'is the supported entry point. Delete the `ai:chat_window` component node and put nothing ' + + "in its place; to choose which platform agent the overlay answers with, set the app's " + + '`defaultAgent`.'], ['element:form', '`element:form` was removed in @objectstack/spec 17 ' + '(ADR-0049) — the whole `element:form` element is retired: no renderer for it ' + 'ever shipped in objectui, framework or cloud (Studio\'s designer palette lists it as a ' @@ -144,6 +167,12 @@ export const RETIRED_PAGE_COMPONENT_TYPES: ReadonlyMap = new Map * row. (`user:profile` was refused by name from the day it left the enum; the * two elements left the enum first and were refused by name later, once this * map existed to express it.) + * + * `ai:chat_window` REMOVED (#21504, ADR-0049 enforce-or-remove) the + * `user:profile` way: refused by name from the day it left the enum, its row + * kept as a whole-bag refusal. No renderer ever shipped and none is wanted — + * the console's floating chat overlay is the supported AI chat entry point, so + * an inline page-level chat window is not part of the authorable surface. */ export const PageComponentType = z.enum([ // Structure @@ -160,8 +189,11 @@ export const PageComponentType = z.enum([ // Utility — `user:profile` REMOVED (#14159): shell chrome, refused by name // through `RETIRED_PAGE_COMPONENT_TYPES` above, not merely de-advertised. 'global:search', 'global:notifications', - // AI - 'ai:chat_window', 'ai:suggestion', + // AI — `ai:chat_window` REMOVED (#21504, ADR-0049): no renderer by design + // (the floating chat overlay is the entry point), refused by name through + // `RETIRED_PAGE_COMPONENT_TYPES` above, not merely de-advertised. + // `ai:suggestion` stays — it has a placeholder renderer, a different class. + 'ai:suggestion', // Content Elements (Airtable Interface parity) 'element:text', 'element:number', 'element:image', 'element:divider', // Interactive Elements (Phase B — Element Library) @@ -308,7 +340,7 @@ export const PageComponentSchema = lazySchema(() => strictObject({ if (guidance) { ctx.addIssue({ code: 'custom', message: guidance, params: { retiredComponentType: type } }); } - }).describe('Component Type — a standard vocabulary member, or a custom/registered component type in its own namespace (e.g. `object-grid`, `mcp:connect-agent`). The spec\'s own type namespaces are a closed vocabulary at author time: inside them, a type the vocabulary does not declare is refused by `os validate` / `os build` / `os lint` (rule `component-type-unknown`); a type the vocabulary RETIRED by name (`user:profile` — shell chrome, not author-placeable; `element:filter` and `element:form` — retired whole, no renderer for either ever shipped) is refused at the parse itself, with the retirement prescription.'), + }).describe('Component Type — a standard vocabulary member, or a custom/registered component type in its own namespace (e.g. `object-grid`, `mcp:connect-agent`). The spec\'s own type namespaces are a closed vocabulary at author time: inside them, a type the vocabulary does not declare is refused by `os validate` / `os build` / `os lint` (rule `component-type-unknown`); a type the vocabulary RETIRED by name (`user:profile` — shell chrome, not author-placeable; `ai:chat_window` — no renderer by design, the floating chat overlay is the AI chat entry point; `element:filter` and `element:form` — retired whole, no renderer for either ever shipped) is refused at the parse itself, with the retirement prescription.'), id: z.string().optional().describe('Unique instance ID'), /** Configuration */