From 65402414d43e2c6f33b1e4f5b46a5892f60eee0e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 18:27:58 +0000 Subject: [PATCH 1/4] feat(spec): retire the element:form element at element grain (#9249, ADR-0049) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No renderer or reader anywhere — measured objectstack c684d00cfc, objectui 76ceb1e, cloud 5f1bf23f/a11458b (thread readings). All six ElementFormProps keys are retiredKey tombstones behind the kept ComponentPropsMap row; D2 strip conversion element-form-removed under protocol 18; enum value dropped; lint COMPONENT_FIELD_SPECS entry dropped; three conversion fixtures re-seated off the retired neighbor; docs and baselines regenerated. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01B4h3medzvhB9rpfoja9jcw --- .changeset/element-form-lint-residue.md | 16 ++ .changeset/element-form-retired.md | 88 +++++++++ content/docs/references/ui/component.mdx | 12 +- content/docs/references/ui/page.mdx | 3 +- content/docs/ui/pages.mdx | 2 +- .../lint/src/validate-page-field-bindings.ts | 5 +- .../src/validate-react-page-props.test.ts | 12 +- .../lint/src/validate-react-page-props.ts | 8 +- packages/spec/authorable-defaults/ui.json | 1 - packages/spec/authorable-surface/ui.json | 12 +- packages/spec/scripts/format-type.test.ts | 2 +- packages/spec/src/conversions/registry.ts | 177 +++++++++++++++++- .../18.ui__ElementFormProps__aria.ts | 24 +++ .../18.ui__ElementFormProps__fields.ts | 24 +++ .../18.ui__ElementFormProps__mode.ts | 24 +++ .../18.ui__ElementFormProps__object.ts | 24 +++ .../18.ui__ElementFormProps__onSubmit.ts | 24 +++ .../18.ui__ElementFormProps__submitLabel.ts | 24 +++ packages/spec/src/migrations/registry.ts | 144 ++++++++++++++ .../spec/src/system/i18n-resolver.test.ts | 7 +- packages/spec/src/system/translation.zod.ts | 4 +- packages/spec/src/ui/component.test.ts | 67 ++++--- packages/spec/src/ui/component.zod.ts | 63 ++++++- packages/spec/src/ui/page.zod.ts | 5 +- 24 files changed, 695 insertions(+), 77 deletions(-) create mode 100644 .changeset/element-form-lint-residue.md create mode 100644 .changeset/element-form-retired.md create mode 100644 packages/spec/src/migrations/entries/retired-keys/18.ui__ElementFormProps__aria.ts create mode 100644 packages/spec/src/migrations/entries/retired-keys/18.ui__ElementFormProps__fields.ts create mode 100644 packages/spec/src/migrations/entries/retired-keys/18.ui__ElementFormProps__mode.ts create mode 100644 packages/spec/src/migrations/entries/retired-keys/18.ui__ElementFormProps__object.ts create mode 100644 packages/spec/src/migrations/entries/retired-keys/18.ui__ElementFormProps__onSubmit.ts create mode 100644 packages/spec/src/migrations/entries/retired-keys/18.ui__ElementFormProps__submitLabel.ts diff --git a/.changeset/element-form-lint-residue.md b/.changeset/element-form-lint-residue.md new file mode 100644 index 0000000000..857779abc1 --- /dev/null +++ b/.changeset/element-form-lint-residue.md @@ -0,0 +1,16 @@ +--- +"@objectstack/lint": patch +--- + +fix(lint): drop the `element:form` entry from `COMPONENT_FIELD_SPECS` (#9249) + +The whole `element:form` element retired at element grain (ADR-0049 — no +renderer ever shipped for it; the #9220 shape one element over), so every +`ElementFormProps` key is a `retiredKey()` tombstone and no spec-conformant +page carries `fields` on it. The field-binding rule's job (resolve a field +NAME against the object) is not the question a retired key raises: an authored +key is already reported by name with the element-retirement prescription — +which names the live replacement, the object-bound `object-form` block — +through the #5068 props gate, and the binding entry would only add a second +finding about a key that no longer exists — the #5775/#6629 residue class the +package's own `component-field-specs-liveness` gate refuses. diff --git a/.changeset/element-form-retired.md b/.changeset/element-form-retired.md new file mode 100644 index 0000000000..3cf18d05c5 --- /dev/null +++ b/.changeset/element-form-retired.md @@ -0,0 +1,88 @@ +--- +"@objectstack/spec": minor +--- + +feat(spec): retire the `element:form` element at element grain (#9249, ADR-0049) + + + +**BREAKING** accept-set narrowing, landing after the v17.0.0 cut (the lockstep +launch-window convention ships it as `minor`; the migration prescription is +registered under protocol major 18, where `os migrate meta` users will look). + +`element:form` never had a renderer or reader anywhere — the #9220 +(`element:filter`) shape one element over, recorded by that card's own verdict +sweep. Measured at retirement (objectstack `c684d00cfc`, objectui `76ceb1e`; +cloud per the origin card's two recorded readings at `5f1bf23f` and `a11458b`, +positive controls passing): objectui registers no renderer for it — its +`renderers/basic/elements.tsx` header deferred the element to "owning plugins" +that never materialized — and Studio's designer palette carries it as a +no-renderer `PALETTE_EXCLUSIONS` entry that names the live replacement ("no +renderer — use the object-bound `object-form` block"). The 2026-06 +page-liveness audit already recorded it rendering "Unknown component type". So +the full form contract `ElementFormPropsSchema` declared — `object`, `fields`, +`mode`, `submitLabel`, `onSubmit` (CEL), `aria` — was a capability claim +nothing kept. + +**What is refused:** every authored `element:form` key. All six are +`retiredKey()` tombstones, so authoring one is a `tsc` error and a parse error +carrying the prescription (fully-qualified key, why the element is dead, the +replacement, the `os migrate meta` pointer), dispatched through the KEPT +`ComponentPropsMap` row — deleting the row would demote the type to an +unregistered custom string the #5068 props gate deliberately skips. + +**What stays accepted:** a bare `element:form` node with empty `properties` +(the migrated shape — the open `type` union accepts any string, and deleting +authored page nodes is a layout decision a mechanical conversion must not +make). It renders nothing, exactly as it always did. + +The retirement kit: + +- `retiredKey()` tombstones for all six keys at the schema + (`packages/spec/src/ui/component.zod.ts`); the `PageComponentType` enum + drops the value (de-advertisement — the open string arm still accepts it) +- ADR-0087 registration: six retired-key entries + `ui/ElementFormProps:{aria,fields,mode,object,onSubmit,submitLabel}` and the + D2 conversion `element-form-removed` (protocol 18), wired into the step-18 + chain — `os migrate meta --from 17` strips all six keys from authored + `element:form` blocks (pure lossless deletes; none ever had an effect to + lose) and leaves the bare node +- pin tests (`component.test.ts` — refusal pins assert the prescription per + key; a positive pin parses the bare migrated node clean and asserts nothing + materializes; the kept-map-row pin flips from parse to refusal) +- `packages/lint`'s `COMPONENT_FIELD_SPECS` entry drops (its own changeset); + the component-translation `submitLabel` describes stop naming `element:form` + as carrier (the orphaned-key decision is #10926, out of this card's scope) +- generated baselines/docs follow the schema (authorable surface/defaults, + spec-changes, upgrade guide, reference docs) +- objectui's `elements.tsx` comment and Studio palette-exclusion lines are + cross-repo and already queued under objectui#4935 + +## FROM → TO + +```ts +// before — parsed green; nothing anywhere rendered it, so the page showed +// "Unknown component type" where the author expected a form +{ + type: 'element:form', + properties: { + object: 'lead', + fields: ['name', 'email'], + mode: 'create', + submitLabel: 'Create Lead', + }, +} + +// after — use the object-bound `object-form` block (#7751): rendered, +// designer-publishable, same intent (`objectName`, `fields`, `mode`, +// `submitText`) +{ + type: 'object-form', + properties: { + objectName: 'lead', + fields: ['name', 'email'], + mode: 'create', + submitText: 'Create Lead', + }, +} +``` diff --git a/content/docs/references/ui/component.mdx b/content/docs/references/ui/component.mdx index 5269c1a6e3..092b0e508b 100644 --- a/content/docs/references/ui/component.mdx +++ b/content/docs/references/ui/component.mdx @@ -77,12 +77,12 @@ const result = AIChatWindowProps.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **object** | `string` | ✅ | Object for the form | -| **fields** | `string[]` | optional | Fields to display (defaults to all editable fields) | -| **mode** | `Enum<'create' \| 'edit'>` | optional (default: `"create"`) | Form mode | -| **submitLabel** | `string \| Record` | optional | Submit button label | -| **onSubmit** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Action expression on form submit (CEL) | -| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +| **object** | `never` | optional | [REMOVED] `element:form` property `object` was removed in @objectstack/spec 17 (#9249, 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 no-renderer exclusion — "use the object-bound `object-form` block"), so every key on this element was a capability claim nothing kept. Delete the `element:form` component and use the object-bound `object-form` block instead (#7751) — it is rendered, designer-publishable, and carries the same intent (`objectName`, `fields`, `mode`, `submitText`). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **fields** | `never` | optional | [REMOVED] `element:form` property `fields` was removed in @objectstack/spec 17 (#9249, 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 no-renderer exclusion — "use the object-bound `object-form` block"), so every key on this element was a capability claim nothing kept. Delete the `element:form` component and use the object-bound `object-form` block instead (#7751) — it is rendered, designer-publishable, and carries the same intent (`objectName`, `fields`, `mode`, `submitText`). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **mode** | `never` | optional | [REMOVED] `element:form` property `mode` was removed in @objectstack/spec 17 (#9249, 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 no-renderer exclusion — "use the object-bound `object-form` block"), so every key on this element was a capability claim nothing kept. Delete the `element:form` component and use the object-bound `object-form` block instead (#7751) — it is rendered, designer-publishable, and carries the same intent (`objectName`, `fields`, `mode`, `submitText`). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **submitLabel** | `never` | optional | [REMOVED] `element:form` property `submitLabel` was removed in @objectstack/spec 17 (#9249, 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 no-renderer exclusion — "use the object-bound `object-form` block"), so every key on this element was a capability claim nothing kept. Delete the `element:form` component and use the object-bound `object-form` block instead (#7751) — it is rendered, designer-publishable, and carries the same intent (`objectName`, `fields`, `mode`, `submitText`). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **onSubmit** | `never` | optional | [REMOVED] `element:form` property `onSubmit` was removed in @objectstack/spec 17 (#9249, 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 no-renderer exclusion — "use the object-bound `object-form` block"), so every key on this element was a capability claim nothing kept. Delete the `element:form` component and use the object-bound `object-form` block instead (#7751) — it is rendered, designer-publishable, and carries the same intent (`objectName`, `fields`, `mode`, `submitText`). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **aria** | `never` | optional | [REMOVED] `element:form` property `aria` was removed in @objectstack/spec 17 (#9249, 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 no-renderer exclusion — "use the object-bound `object-form` block"), so every key on this element was a capability claim nothing kept. Delete the `element:form` component and use the object-bound `object-form` block instead (#7751) — it is rendered, designer-publishable, and carries the same intent (`objectName`, `fields`, `mode`, `submitText`). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | --- diff --git a/content/docs/references/ui/page.mdx b/content/docs/references/ui/page.mdx index 22221d659f..766d0a69b5 100644 --- a/content/docs/references/ui/page.mdx +++ b/content/docs/references/ui/page.mdx @@ -105,7 +105,7 @@ Interface-level page configuration (Airtable parity) | 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' \| 'user:profile' \| 'ai:chat_window' \| 'ai:suggestion' \| 'element:text' \| 'element:number' \| 'element:image' \| 'element:divider' \| 'element:button' \| 'element:form' \| 'element:record_picker' \| 'element:text_input'> \| string` | ✅ | Component Type (Standard enum or custom string) | +| **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' \| 'user:profile' \| 'ai:chat_window' \| 'ai:suggestion' \| 'element:text' \| 'element:number' \| 'element:image' \| 'element:divider' \| 'element:button' \| 'element:record_picker' \| 'element:text_input'> \| string` | ✅ | Component Type (Standard enum or custom string) | | **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. | @@ -157,7 +157,6 @@ Interface-level page configuration (Airtable parity) * `element:image` * `element:divider` * `element:button` -* `element:form` * `element:record_picker` * `element:text_input` diff --git a/content/docs/ui/pages.mdx b/content/docs/ui/pages.mdx index ae7f2ee7ba..c7d8a136d3 100644 --- a/content/docs/ui/pages.mdx +++ b/content/docs/ui/pages.mdx @@ -186,7 +186,7 @@ The `type` field is a union of the standard `PageComponentType` enum and any cus - **Navigation:** `app:launcher`, `nav:menu`, `nav:breadcrumb` - **Utility:** `global:search`, `global:notifications`, `user:profile` - **AI:** `ai:chat_window`, `ai:suggestion` -- **Elements:** `element:text`, `element:number`, `element:image`, `element:divider`, `element:button`, `element:form`, `element:record_picker`, `element:text_input` (`element:filter` was retired in v17.x — no renderer ever shipped for it; list surfaces own their filtering via a view's `userFilters` quick-filter bar or the list toolbar's filter builder) +- **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. 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), `responsive`, and `aria` configuration. Custom string types are also accepted for project-specific widgets. diff --git a/packages/lint/src/validate-page-field-bindings.ts b/packages/lint/src/validate-page-field-bindings.ts index 4357734b1a..55ed8bfd78 100644 --- a/packages/lint/src/validate-page-field-bindings.ts +++ b/packages/lint/src/validate-page-field-bindings.ts @@ -197,7 +197,10 @@ export const COMPONENT_FIELD_SPECS: Readonly> // spec-conformant page carries `fields` on it, and the #5068 props gate // reports an authored one by name with the element-retirement prescription — // the same #5775/#6629 residue class as the record-picker entries below. - 'element:form': { props: ['fields'] }, + // `element:form` had the same `{ props: ['fields'] }` entry until #9249 + // retired that element the same way (ADR-0049, the #9220 shape one element + // over); its tombstones' prescription names the live replacement, the + // object-bound `object-form` block. // `labelField` is the one field-bearing prop this element declares. Its former // companions `displayField` (renamed to `labelField`, ADR-0087 D2) and // `searchFields` (deleted, ADR-0049) were retired in #5775 and are diff --git a/packages/lint/src/validate-react-page-props.test.ts b/packages/lint/src/validate-react-page-props.test.ts index db928eac33..abb306b955 100644 --- a/packages/lint/src/validate-react-page-props.test.ts +++ b/packages/lint/src/validate-react-page-props.test.ts @@ -721,14 +721,16 @@ describe('validateReactPageProps — record:* blocks need a context this surface describe('validateReactPageProps — escape hatch (#4340)', () => { it('checks the props bag by the registered type the author names', () => { + // `element:number` carries the pin since #9249 retired `element:form` + // (whose `fields` entry left COMPONENT_FIELD_SPECS with the element). const f = unknownFields( validateReactPageProps( - propsPage(jsx('Block', `type="element:form" objectName="crm_account" fields={['nope']}`)), + propsPage(jsx('Block', `type="element:number" objectName="crm_account" field="nope"`)), ), ); expect(f).toHaveLength(1); expect(f[0].where).toBe('page "p" › '); - expect(f[0].path).toBe('pages[0].source › fields[0]'); + expect(f[0].path).toBe('pages[0].source › field'); }); it('skips a type with no descriptor, and a non-static type', () => { @@ -1332,15 +1334,17 @@ describe('validateReactPageProps — unprovisioned injected anchors (#8340)', () // The shared table `validate-page-field-bindings` walks; on this surface // it is reachable only by the type the author spells out, which is what // makes the escape hatch checked rather than a hole. + // `element:number` carries the pin since #9249 retired `element:form` + // (whose `fields` entry left COMPONENT_FIELD_SPECS with the element). const f = validateReactPageProps( - extPage(`function Page(){ return ; }`), + extPage(`function Page(){ return ; }`), ); expect(f.filter((x) => x.rule === PAGE_FIELD_UNKNOWN)).toHaveLength(0); const warned = f.filter((x) => x.rule === PAGE_FIELD_UNPROVISIONED); expect(warned).toHaveLength(1); expect(warned[0].severity).toBe('warning'); expect(warned[0].where).toBe('page "p" › '); - expect(warned[0].path).toBe('pages[0].source › fields[0]'); + expect(warned[0].path).toBe('pages[0].source › field'); expect(warned[0].message).toContain('external object (ADR-0015)'); }); }); diff --git a/packages/lint/src/validate-react-page-props.ts b/packages/lint/src/validate-react-page-props.ts index fa986d97a8..3f32f3b45f 100644 --- a/packages/lint/src/validate-react-page-props.ts +++ b/packages/lint/src/validate-react-page-props.ts @@ -608,7 +608,7 @@ function checkObjectChart( // // `REACT_FIELD_SPECS` below describes the blocks whose metadata twin lives // under different prop names — `` (twin: a list page's -// `interfaceConfig`) and `` (twin: `element:form` + the +// `interfaceConfig`) and `` (twin: the `object-form` block + the // form-layout rule) — plus ``'s `filter`. // // `COMPONENT_FIELD_SPECS`, the table `validate-page-field-bindings` walks on @@ -883,9 +883,9 @@ function checkBlockFieldProps( ); } - // `` renders the registered component the author - // names; every other block's type is fixed by its tag. A `record:*` type - // never arrives here — `recordContextFinding` rejected it upstream. + // `` renders the registered component the + // author names; every other block's type is fixed by its tag. A `record:*` + // type never arrives here — `recordContextFinding` rejected it upstream. const schemaType = tag === 'Block' ? strOf(values.get('type')) : SCHEMA_TYPE_BY_TAG.get(tag); if (schemaType && COMPONENT_FIELD_SPECS[schemaType]) { out.push( diff --git a/packages/spec/authorable-defaults/ui.json b/packages/spec/authorable-defaults/ui.json index 6a7edd4d6b..d24f6db559 100644 --- a/packages/spec/authorable-defaults/ui.json +++ b/packages/spec/authorable-defaults/ui.json @@ -34,7 +34,6 @@ "ui/ElementButtonProps:iconPosition = \"left\"", "ui/ElementButtonProps:size = \"medium\"", "ui/ElementButtonProps:variant = \"primary\"", - "ui/ElementFormProps:mode = \"create\"", "ui/ElementImageProps:fit = \"cover\"", "ui/ElementMetadataViewerProps:detail = \"business\"", "ui/ElementTextInputProps:disabled = false", diff --git a/packages/spec/authorable-surface/ui.json b/packages/spec/authorable-surface/ui.json index 57d99fd848..75623770a1 100644 --- a/packages/spec/authorable-surface/ui.json +++ b/packages/spec/authorable-surface/ui.json @@ -361,12 +361,12 @@ "ui/ElementFilterProps:object [RETIRED]", "ui/ElementFilterProps:showSearch [RETIRED]", "ui/ElementFilterProps:targetVariable [RETIRED]", - "ui/ElementFormProps:aria", - "ui/ElementFormProps:fields", - "ui/ElementFormProps:mode", - "ui/ElementFormProps:object", - "ui/ElementFormProps:onSubmit", - "ui/ElementFormProps:submitLabel", + "ui/ElementFormProps:aria [RETIRED]", + "ui/ElementFormProps:fields [RETIRED]", + "ui/ElementFormProps:mode [RETIRED]", + "ui/ElementFormProps:object [RETIRED]", + "ui/ElementFormProps:onSubmit [RETIRED]", + "ui/ElementFormProps:submitLabel [RETIRED]", "ui/ElementImageProps:alt", "ui/ElementImageProps:aria", "ui/ElementImageProps:fit", diff --git a/packages/spec/scripts/format-type.test.ts b/packages/spec/scripts/format-type.test.ts index 83445f8000..2f6d7edeef 100644 --- a/packages/spec/scripts/format-type.test.ts +++ b/packages/spec/scripts/format-type.test.ts @@ -1353,7 +1353,7 @@ const PAGE_COMPONENT = { 'app:launcher', 'nav:menu', 'nav:breadcrumb', 'global:search', 'global:notifications', 'user:profile', 'ai:chat_window', 'ai:suggestion', 'element:text', 'element:number', 'element:image', 'element:divider', - 'element:button', 'element:form', 'element:record_picker', + 'element:button', 'element:record_picker', 'element:text_input', ], }, diff --git a/packages/spec/src/conversions/registry.ts b/packages/spec/src/conversions/registry.ts index 0361fc6e52..942427c824 100644 --- a/packages/spec/src/conversions/registry.ts +++ b/packages/spec/src/conversions/registry.ts @@ -5187,7 +5187,10 @@ const recordPickerDisplayFieldToLabelField: MetadataConversion = { { type: 'element:record_picker', properties: { object: 'b', labelField: 'name', displayField: 'title' } }, // `displayField` is a live LOOKUP-FIELD key elsewhere on the // surface — a different component's business, untouched here. - { type: 'element:form', properties: { object: 'c', displayField: 'title' } }, + // (The neighbor was `element:form` until #9249 retired that + // element whole; its strip conversion would now touch the node, + // so the overlap rides on a type no conversion dispatches on.) + { type: 'element:text', properties: { displayField: 'title' } }, // Nested one container down (#6775) — a picker inside a card is // where a form-shaped page actually puts one. { @@ -5227,7 +5230,7 @@ const recordPickerDisplayFieldToLabelField: MetadataConversion = { { type: 'element:record_picker', properties: { object: 'showcase_project', labelField: 'title' } }, { type: 'element:record_picker', properties: { object: 'a', labelField: 'name' } }, { type: 'element:record_picker', properties: { object: 'b', labelField: 'name', displayField: 'title' } }, - { type: 'element:form', properties: { object: 'c', displayField: 'title' } }, + { type: 'element:text', properties: { displayField: 'title' } }, { type: 'page:card', properties: { @@ -5301,8 +5304,11 @@ const recordPickerInertKeysRemoved: MetadataConversion = { properties: { object: 'showcase_project', searchFields: ['name', 'code'], multiple: true }, }, // `multiple` is a live FIELD key (lookup fields) — a different - // surface entirely, and not this entry's business. - { type: 'element:form', properties: { object: 'a', multiple: true } }, + // surface entirely, and not this entry's business. (The + // neighbor was `element:form` until #9249 retired that element + // whole; the overlap now rides on a type no conversion + // dispatches on.) + { type: 'element:text', properties: { multiple: true } }, // Inside a tab panel (#6775): `page:tabs` hangs its sub-tree off // `properties.items[].children`, which the walk now descends. { @@ -5343,7 +5349,7 @@ const recordPickerInertKeysRemoved: MetadataConversion = { name: 'main', components: [ { type: 'element:record_picker', properties: { object: 'showcase_project' } }, - { type: 'element:form', properties: { object: 'a', multiple: true } }, + { type: 'element:text', properties: { multiple: true } }, { type: 'page:tabs', properties: { @@ -6946,9 +6952,11 @@ const elementFilterRemoved: MetadataConversion = { }, }, // Key overlap on a DIFFERENT type rides through untouched — - // `object` is also an `element:form` prop, and the strip - // dispatches on the component type, not the key name. - { type: 'element:form', properties: { object: 'order' } }, + // `object` is also a live `element:record_picker` prop, and + // the strip dispatches on the component type, not the key + // name. (The neighbor was `element:form` until #9249 retired + // that element whole; its own strip would now touch the node.) + { type: 'element:record_picker', properties: { object: 'order' } }, // Nested one container down (#6775) — the walk descends. { type: 'page:card', @@ -6988,7 +6996,7 @@ const elementFilterRemoved: MetadataConversion = { name: 'main', components: [ { type: 'element:filter', properties: {} }, - { type: 'element:form', properties: { object: 'order' } }, + { type: 'element:record_picker', properties: { object: 'order' } }, { type: 'page:card', properties: { @@ -7015,7 +7023,155 @@ const elementFilterRemoved: MetadataConversion = { ], }, // One per stripped key: 5 on the top-level node, 2 on the nested one, - // 3 on the slotted one. The `element:form` neighbor is untouched. + // 3 on the slotted one. The `element:record_picker` neighbor is untouched. + expectedNotices: 10, + }, +}; + +/** + * `element:form` — the whole element retired (protocol 18, #9249, ADR-0049 + * enforce-or-remove at ELEMENT grain). + * + * The #9220 shape one element over, recorded by that card's own verdict sweep + * and re-measured at retirement time (objectstack @c684d00cfc, objectui + * @76ceb1e; cloud per the card's two recorded readings @5f1bf23f / @a11458b): + * NO renderer or behavior reader of `element:form` exists anywhere. objectui + * registers no renderer (its `renderers/basic/elements.tsx` header deferred + * the element to "owning plugins" that never materialized), Studio's designer + * palette carries it as a no-renderer PALETTE_EXCLUSIONS entry naming the live + * replacement ("no renderer — use the object-bound `object-form` block"), and + * the 2026-06 page-liveness audit recorded it rendering "Unknown component + * type". Every key was a capability claim nothing kept — per-key retirement + * would have been the wrong grain, so all six authorable keys are tombstoned + * together and this conversion strips them together. + * + * Pure lossless deletes — no key ever had an effect to lose. The component + * node itself is NOT removed: the open `type` union tolerates a bare inert + * node (nothing rendered it before either), and deleting authored page nodes + * is a layout decision a mechanical conversion must not make. The + * prescription tells the author to delete the component and use the + * object-bound `object-form` block (#7751) instead — rendered, + * designer-publishable, and carrying the same intent (`objectName`, `fields`, + * `mode`, `submitText`). + */ +const elementFormRemoved: MetadataConversion = { + id: 'element-form-removed', + toMajor: 18, + retiredFromLoadPath: true, + surface: + 'page.component.element:form.object / page.component.element:form.fields / ' + + 'page.component.element:form.mode / page.component.element:form.submitLabel / ' + + 'page.component.element:form.onSubmit / page.component.element:form.aria', + summary: + "the whole 'element:form' element retired (#9249 — no renderer for it ever shipped in " + + 'any repo, so every key was a capability claim nothing kept; use the object-bound ' + + "'object-form' block instead — rendered and designer-publishable). All six props are " + + 'stripped; the bare node stays, inert as it always was', + apply(stack, emit) { + return mapPageComponents(stack, (component, path) => { + if (component.type !== 'element:form') return component; + const properties = component.properties; + if (!isDict(properties)) return component; + const stripped = stripKeys( + properties, + ['object', 'fields', 'mode', 'submitLabel', 'onSubmit', 'aria'], + emit, + `${path}.properties`, + ); + if (stripped === properties) return component; + return { ...component, properties: stripped }; + }); + }, + fixture: { + before: { + pages: [ + { + name: 'lead_board', + regions: [ + { + name: 'main', + components: [ + { + type: 'element:form', + properties: { + object: 'lead', + fields: ['name', 'email'], + mode: 'edit', + submitLabel: 'Update Lead', + onSubmit: 'navigate_to("lead_detail")', + }, + }, + // Key overlap on a DIFFERENT type rides through untouched — + // `object` is also a live `element:record_picker` prop, and + // the strip dispatches on the component type, not the key name. + { type: 'element:record_picker', properties: { object: 'lead' } }, + // Nested one container down (#6775) — the walk descends. + { + type: 'page:card', + properties: { + title: 'New lead', + children: [ + { type: 'element:form', properties: { object: 'lead', fields: ['name'] } }, + ], + }, + }, + ], + }, + ], + }, + // The named-slot shape, on a slotted record page. + { + name: 'lead_detail', + kind: 'slotted', + regions: [], + slots: { + side: [ + { + type: 'element:form', + properties: { object: 'lead', mode: 'create', aria: { label: 'Quick create' } }, + }, + ], + }, + }, + ], + }, + after: { + pages: [ + { + name: 'lead_board', + regions: [ + { + name: 'main', + components: [ + { type: 'element:form', properties: {} }, + { type: 'element:record_picker', properties: { object: 'lead' } }, + { + type: 'page:card', + properties: { + title: 'New lead', + children: [ + { type: 'element:form', properties: {} }, + ], + }, + }, + ], + }, + ], + }, + { + name: 'lead_detail', + kind: 'slotted', + regions: [], + slots: { + side: [ + { type: 'element:form', properties: {} }, + ], + }, + }, + ], + }, + // One per stripped key: 5 on the top-level node, 2 on the nested one, + // 3 on the slotted one. The `element:record_picker` neighbor is untouched. expectedNotices: 10, }, }; @@ -7477,6 +7633,7 @@ export const CONVERSIONS_BY_MAJOR: Readonly> // Sources are rewritten by the D2 conversion `element-filter-removed`, which // strips all six keys and leaves the bare node — inert as it always was. 'ui/ElementFilterProps:targetVariable', + // #9249 — ADR-0049 enforce-or-remove at ELEMENT grain. `element:form` never + // had a renderer or reader anywhere — the #9220 shape one element over, + // recorded by that card's own verdict sweep. objectui registers none (its + // renderers/basic/elements.tsx header deferred the element to "owning plugins" + // that never materialized), Studio's designer palette carries it as a + // no-renderer PALETTE_EXCLUSIONS entry naming the live replacement ("no + // renderer — use the object-bound `object-form` block"), and the 2026-06 + // page-liveness audit recorded it rendering "Unknown component type". Measured + // at retirement time (objectstack @c684d00cfc, objectui @76ceb1e; cloud per + // the card's two recorded readings @5f1bf23f / @a11458b): zero production + // readers of any `element:form` key — so every key, this one included, was a + // capability claim nothing kept. Per-key retirement would have been the wrong + // grain (the #9198 lesson, two elements over); all six authorable keys are + // tombstoned together. + // + // Registered under 18, not 17: v17.0.0 was cut before this landed, so the + // tombstones ship 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 (the #8495 / PR #8666 precedent). + // Sources are rewritten by the D2 conversion `element-form-removed`, which + // strips all six keys and leaves the bare node — inert as it always was. + 'ui/ElementFormProps:aria', + // #9249 — ADR-0049 enforce-or-remove at ELEMENT grain. `element:form` never + // had a renderer or reader anywhere — the #9220 shape one element over, + // recorded by that card's own verdict sweep. objectui registers none (its + // renderers/basic/elements.tsx header deferred the element to "owning plugins" + // that never materialized), Studio's designer palette carries it as a + // no-renderer PALETTE_EXCLUSIONS entry naming the live replacement ("no + // renderer — use the object-bound `object-form` block"), and the 2026-06 + // page-liveness audit recorded it rendering "Unknown component type". Measured + // at retirement time (objectstack @c684d00cfc, objectui @76ceb1e; cloud per + // the card's two recorded readings @5f1bf23f / @a11458b): zero production + // readers of any `element:form` key — so every key, this one included, was a + // capability claim nothing kept. Per-key retirement would have been the wrong + // grain (the #9198 lesson, two elements over); all six authorable keys are + // tombstoned together. + // + // Registered under 18, not 17: v17.0.0 was cut before this landed, so the + // tombstones ship 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 (the #8495 / PR #8666 precedent). + // Sources are rewritten by the D2 conversion `element-form-removed`, which + // strips all six keys and leaves the bare node — inert as it always was. + 'ui/ElementFormProps:fields', + // #9249 — ADR-0049 enforce-or-remove at ELEMENT grain. `element:form` never + // had a renderer or reader anywhere — the #9220 shape one element over, + // recorded by that card's own verdict sweep. objectui registers none (its + // renderers/basic/elements.tsx header deferred the element to "owning plugins" + // that never materialized), Studio's designer palette carries it as a + // no-renderer PALETTE_EXCLUSIONS entry naming the live replacement ("no + // renderer — use the object-bound `object-form` block"), and the 2026-06 + // page-liveness audit recorded it rendering "Unknown component type". Measured + // at retirement time (objectstack @c684d00cfc, objectui @76ceb1e; cloud per + // the card's two recorded readings @5f1bf23f / @a11458b): zero production + // readers of any `element:form` key — so every key, this one included, was a + // capability claim nothing kept. Per-key retirement would have been the wrong + // grain (the #9198 lesson, two elements over); all six authorable keys are + // tombstoned together. + // + // Registered under 18, not 17: v17.0.0 was cut before this landed, so the + // tombstones ship 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 (the #8495 / PR #8666 precedent). + // Sources are rewritten by the D2 conversion `element-form-removed`, which + // strips all six keys and leaves the bare node — inert as it always was. + 'ui/ElementFormProps:mode', + // #9249 — ADR-0049 enforce-or-remove at ELEMENT grain. `element:form` never + // had a renderer or reader anywhere — the #9220 shape one element over, + // recorded by that card's own verdict sweep. objectui registers none (its + // renderers/basic/elements.tsx header deferred the element to "owning plugins" + // that never materialized), Studio's designer palette carries it as a + // no-renderer PALETTE_EXCLUSIONS entry naming the live replacement ("no + // renderer — use the object-bound `object-form` block"), and the 2026-06 + // page-liveness audit recorded it rendering "Unknown component type". Measured + // at retirement time (objectstack @c684d00cfc, objectui @76ceb1e; cloud per + // the card's two recorded readings @5f1bf23f / @a11458b): zero production + // readers of any `element:form` key — so every key, this one included, was a + // capability claim nothing kept. Per-key retirement would have been the wrong + // grain (the #9198 lesson, two elements over); all six authorable keys are + // tombstoned together. + // + // Registered under 18, not 17: v17.0.0 was cut before this landed, so the + // tombstones ship 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 (the #8495 / PR #8666 precedent). + // Sources are rewritten by the D2 conversion `element-form-removed`, which + // strips all six keys and leaves the bare node — inert as it always was. + 'ui/ElementFormProps:object', + // #9249 — ADR-0049 enforce-or-remove at ELEMENT grain. `element:form` never + // had a renderer or reader anywhere — the #9220 shape one element over, + // recorded by that card's own verdict sweep. objectui registers none (its + // renderers/basic/elements.tsx header deferred the element to "owning plugins" + // that never materialized), Studio's designer palette carries it as a + // no-renderer PALETTE_EXCLUSIONS entry naming the live replacement ("no + // renderer — use the object-bound `object-form` block"), and the 2026-06 + // page-liveness audit recorded it rendering "Unknown component type". Measured + // at retirement time (objectstack @c684d00cfc, objectui @76ceb1e; cloud per + // the card's two recorded readings @5f1bf23f / @a11458b): zero production + // readers of any `element:form` key — so every key, this one included, was a + // capability claim nothing kept. Per-key retirement would have been the wrong + // grain (the #9198 lesson, two elements over); all six authorable keys are + // tombstoned together. + // + // Registered under 18, not 17: v17.0.0 was cut before this landed, so the + // tombstones ship 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 (the #8495 / PR #8666 precedent). + // Sources are rewritten by the D2 conversion `element-form-removed`, which + // strips all six keys and leaves the bare node — inert as it always was. + 'ui/ElementFormProps:onSubmit', + // #9249 — ADR-0049 enforce-or-remove at ELEMENT grain. `element:form` never + // had a renderer or reader anywhere — the #9220 shape one element over, + // recorded by that card's own verdict sweep. objectui registers none (its + // renderers/basic/elements.tsx header deferred the element to "owning plugins" + // that never materialized), Studio's designer palette carries it as a + // no-renderer PALETTE_EXCLUSIONS entry naming the live replacement ("no + // renderer — use the object-bound `object-form` block"), and the 2026-06 + // page-liveness audit recorded it rendering "Unknown component type". Measured + // at retirement time (objectstack @c684d00cfc, objectui @76ceb1e; cloud per + // the card's two recorded readings @5f1bf23f / @a11458b): zero production + // readers of any `element:form` key — so every key, this one included, was a + // capability claim nothing kept. Per-key retirement would have been the wrong + // grain (the #9198 lesson, two elements over); all six authorable keys are + // tombstoned together. + // + // Registered under 18, not 17: v17.0.0 was cut before this landed, so the + // tombstones ship 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 (the #8495 / PR #8666 precedent). + // Sources are rewritten by the D2 conversion `element-form-removed`, which + // strips all six keys and leaves the bare node — inert as it always was. + 'ui/ElementFormProps:submitLabel', // #9198 — ADR-0049 enforce-or-remove. `targetVariable` on // `element:record_picker` was a declarative hint with zero readers: the picker // writes the selected record id through the reverse binding — the page diff --git a/packages/spec/src/system/i18n-resolver.test.ts b/packages/spec/src/system/i18n-resolver.test.ts index f06feb18cc..bc9d1dfc34 100644 --- a/packages/spec/src/system/i18n-resolver.test.ts +++ b/packages/spec/src/system/i18n-resolver.test.ts @@ -1103,7 +1103,12 @@ describe('translatePage', () => { { type: 'element:kpi', id: 'kpi_revenue_won', properties: { label: 'Revenue (Won)', value: 42 } }, { type: 'page:card', id: 'ai_briefing', properties: { title: 'Ask the AI Assistant', description: 'Open the assistant panel from the right edge…' } }, { type: 'element:record_picker', id: 'lead_picker', properties: { object: 'lead', placeholder: 'Search leads…', emptyText: 'No records' } }, - { type: 'element:form', id: 'new_lead_form', properties: { object: 'lead', submitLabel: 'Create' } }, + // Was `element:form` until #9249 retired that element whole; the + // resolver is id-addressed and type-agnostic, and the copy-key face + // documents bespoke component types as a legal route for the same + // vocabulary — so the `submitLabel` pin rides one of those, pending + // the #10926 carrier decision. + { type: 'hotcrm:quick_form', id: 'new_lead_form', properties: { object: 'lead', submitLabel: 'Create' } }, { type: 'page:card', id: 'untranslated_card', properties: { title: 'Still English' } }, ], }], diff --git a/packages/spec/src/system/translation.zod.ts b/packages/spec/src/system/translation.zod.ts index 84e1c9e61f..dca3c63ed4 100644 --- a/packages/spec/src/system/translation.zod.ts +++ b/packages/spec/src/system/translation.zod.ts @@ -573,7 +573,7 @@ const translationDataShape = () => ({ * | `description` | `element:text_input` | * | `placeholder` | `element:record_picker`, `element:text_input` | * | `emptyText` | `element:record_picker` | - * | `submitLabel` | `element:form` | + * | `submitLabel` | none since #9249 retired `element:form`, its only declarer — kept pending the #10926 decision (retire vs re-anchor); the bespoke-component route below still speaks it | * * Two deliberate exclusions, both of which a mirror of the issue's proposed * shape would have got wrong: @@ -615,7 +615,7 @@ const translationDataShape = () => ({ label: z.string().optional().describe("Translated component label — overlays the component's own `label` when it declares one, else `properties.label`"), placeholder: z.string().optional().describe('Translated input placeholder (`element:record_picker`, `element:text_input`)'), emptyText: z.string().optional().describe('Translated empty-state text (`element:record_picker`)'), - submitLabel: z.string().optional().describe('Translated submit button label (`element:form`)'), + submitLabel: z.string().optional().describe('Translated submit button label — no spec-declared carrier since `element:form` retired (#9249); still the route for a bespoke component speaking the same vocabulary, pending #10926'), })).optional().describe('Per-component copy keyed by component id (`PageComponentSchema.id`)'), })).optional().describe('Page translations keyed by page name'), diff --git a/packages/spec/src/ui/component.test.ts b/packages/spec/src/ui/component.test.ts index c47a4eef57..3a8499f7e9 100644 --- a/packages/spec/src/ui/component.test.ts +++ b/packages/spec/src/ui/component.test.ts @@ -1105,35 +1105,45 @@ describe('Interactive Elements — element:filter (retired, #9220)', () => { // --------------------------------------------------------------------------- // Interactive Elements — element:form // --------------------------------------------------------------------------- -describe('Interactive Elements — element:form', () => { - it('should accept element:form component', () => { +describe('Interactive Elements — element:form (retired, #9249)', () => { + // The node itself stays parseable: the open `type` union accepts any string, + // and the migration leaves a bare inert node behind. + it('accepts a bare element:form node (the migrated shape)', () => { expect(() => PageComponentSchema.parse({ type: 'element:form', - properties: { object: 'contact' }, + properties: {}, })).not.toThrow(); }); - it('should parse form props with defaults', () => { - const props = ElementFormPropsSchema.parse({ object: 'contact' }); - expect(props.object).toBe('contact'); - expect(props.mode).toBe('create'); - }); - - it('should accept full form props', () => { - const props = ElementFormPropsSchema.parse({ - object: 'contact', - fields: ['name', 'email', 'phone'], - mode: 'edit', - submitLabel: 'Update Contact', - onSubmit: 'navigate_to("page_detail")', - }); - expect(props.mode).toBe('edit'); - expect(props.fields).toHaveLength(3); - expect(props.submitLabel).toBe('Update Contact'); - }); - - it('should reject form without object', () => { - expect(() => ElementFormPropsSchema.parse({})).toThrow(); + // Refusal pins — the ELEMENT retired at element grain (#9249): no renderer + // for `element:form` ever shipped anywhere, so every authorable key is a + // retiredKey tombstone whose prescription names the live replacement (the + // object-bound `object-form` block). + it('refuses every retired key with the element-retirement prescription', () => { + expect(() => ElementFormPropsSchema.parse({ object: 'contact' })) + .toThrow(/`element:form` property `object`.*removed.*`element:form` element is retired.*use the object-bound `object-form` block/s); + expect(() => ElementFormPropsSchema.parse({ fields: ['name', 'email'] })) + .toThrow(/`element:form` property `fields`.*removed.*Delete the `element:form` component/s); + expect(() => ElementFormPropsSchema.parse({ mode: 'edit' })) + .toThrow(/`element:form` property `mode`.*removed/s); + expect(() => ElementFormPropsSchema.parse({ submitLabel: 'Update' })) + .toThrow(/`element:form` property `submitLabel`.*removed/s); + expect(() => ElementFormPropsSchema.parse({ onSubmit: 'navigate_to("page_detail")' })) + .toThrow(/`element:form` property `onSubmit`.*removed/s); + expect(() => ElementFormPropsSchema.parse({ aria: { label: 'Form' } })) + .toThrow(/`element:form` property `aria`.*removed.*Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand/s); + }); + + // The migrated shape — `element-form-removed` strips all six keys and + // leaves the bare node — parses clean and materializes nothing. (The + // pre-retirement schema REQUIRED `object` and defaulted `mode`, so `{}` + // used to throw and a parse used to materialize `mode: 'create'`; both + // died with the element.) + it('parses a bare (migrated) node clean and materializes nothing', () => { + const props = ElementFormPropsSchema.parse({}); + for (const key of ['object', 'fields', 'mode', 'submitLabel', 'onSubmit', 'aria']) { + expect(props).not.toHaveProperty(key); + } }); }); @@ -1406,9 +1416,12 @@ describe('ComponentPropsMap interactive elements', () => { })).toThrow(/`element:filter` element is retired/s); }); - it('should parse element:form props', () => { - const result = ComponentPropsMap['element:form'].parse({ object: 'contact' }); - expect(result.object).toBe('contact'); + // Flip of "should parse element:form props" (#9249): same shape as + // element:filter above — the kept row dispatches to tombstones. + it('refuses element:form props through the kept map row (retired, #9249)', () => { + expect(() => ComponentPropsMap['element:form'].parse({ + object: 'contact', + })).toThrow(/`element:form` element is retired/s); }); it('should parse element:record_picker props', () => { diff --git a/packages/spec/src/ui/component.zod.ts b/packages/spec/src/ui/component.zod.ts index b799b6cf34..69754dc765 100644 --- a/packages/spec/src/ui/component.zod.ts +++ b/packages/spec/src/ui/component.zod.ts @@ -1792,18 +1792,54 @@ export const ElementFilterPropsSchema = lazySchema(() => strictObject({ aria: retiredKey(elementFilterRetired('aria')), })); +/** + * One prescription for every retired `element:form` key — the retirement is + * ELEMENT-grain (#9249), so all six keys carry the same story and differ only + * in the fully-qualified key that heads the string (house style, rule 1). + */ +const elementFormRetired = (key: string): string => + '`element:form` property `' + key + '` was removed in @objectstack/spec 17 ' + + '(#9249, 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 ' + + 'no-renderer exclusion — "use the object-bound `object-form` block"), so every key on ' + + 'this element was a capability claim nothing kept. Delete the `element:form` component ' + + 'and use the object-bound `object-form` block instead (#7751) — it is rendered, ' + + 'designer-publishable, and carries the same intent (`objectName`, `fields`, `mode`, ' + + '`submitText`). ' + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.'; + +/** + * RETIRED at element grain (#9249, ADR-0049 enforce-or-remove). `element:form` + * never had a renderer anywhere: objectui registers none (its + * `renderers/basic/elements.tsx` header deferred it to "owning plugins" that + * never materialized — the same sentence whose `element:filter` half #9220 + * falsified), Studio's designer palette lists it as a no-renderer exclusion + * naming the live replacement ("no renderer — use the object-bound + * `object-form` block"), and the 2026-06 page-liveness audit already recorded + * it rendering "Unknown component type". Every key was therefore a capability + * claim nothing kept — the #9220 shape, one element over, exactly as the + * origin card measured it. + * + * The schema (and its `ComponentPropsMap` row) stays exported so the #5068 + * props gate keeps DISPATCHING on `type: 'element:form'` and refusing every + * authored key with the prescription — deleting the row would demote the type + * to an unregistered custom string the gate deliberately skips, turning a loud + * retirement back into a silent no-op. A bare node with empty `properties` + * parses clean (the open `type` union accepts any string, so a node-level + * refusal is not expressible here); the migration strips the keys and leaves + * exactly that bare, inert node. + */ export const ElementFormPropsSchema = lazySchema(() => strictObject({ surface: 'this `element:form`', history: PROPS_HISTORY, guidanceSets: COMPONENT_LEVEL_GUIDANCE, }, { - object: z.string().describe('Object for the form'), - fields: z.array(z.string()).optional().describe('Fields to display (defaults to all editable fields)'), - mode: z.enum(['create', 'edit']).optional().default('create').describe('Form mode'), - submitLabel: I18nLabelSchema.optional().describe('Submit button label'), - onSubmit: ExpressionInputSchema.optional().describe('Action expression on form submit (CEL)'), - /** ARIA accessibility */ - aria: AriaPropsSchema.optional().describe('ARIA accessibility attributes'), + object: retiredKey(elementFormRetired('object')), + fields: retiredKey(elementFormRetired('fields')), + mode: retiredKey(elementFormRetired('mode')), + submitLabel: retiredKey(elementFormRetired('submitLabel')), + onSubmit: retiredKey(elementFormRetired('onSubmit')), + aria: retiredKey(elementFormRetired('aria')), })); /** @@ -1848,9 +1884,12 @@ export const ElementFormPropsSchema = lazySchema(() => strictObject({ * ruling (2026-08-08, direction A) declares the other two, so all four flat * shorthands are contract. Direction B — retiring the whole flat family and * making `dataSource` the single data-binding door — was NOT dropped: it is a - * cross-element decision (`element:form` / `element:filter` carry the same flat - * `object`), tracked as #6590 for v18, and A does not block it. When B lands - * these two retire alongside `object` / `filter` under ADR-0087, together. + * cross-element decision (`element:form` / `element:filter` carried the same + * flat `object` when it was recorded), tracked as #6590 for v18, and A does + * not block it. Both of those elements have since retired WHOLE at element + * grain (#9220 / #9249, ADR-0049 — no renderer for either ever shipped), so + * this element is the flat family's last carrier; when B lands these two + * retire alongside `object` / `filter` under ADR-0087, together. * * Both keys are declared in the shape `ElementDataSourceSchema` already uses * for its own `sort` / `limit`, deliberately: they are the SAME contract read @@ -2499,6 +2538,10 @@ export const ComponentPropsMap = { // prescription; deleting it would demote `element:filter` to an unregistered // custom string the gate deliberately skips. See the schema's own header. 'element:filter': ElementFilterPropsSchema, + // RETIRED at element grain (#9249) — the row STAYS for the same reason as + // `element:filter` above: the #5068 props gate must keep dispatching on the + // type and refusing every authored key with the prescription (which names + // the live replacement, the object-bound `object-form` block below). 'element:form': ElementFormPropsSchema, 'element:record_picker': ElementRecordPickerPropsSchema, 'element:text_input': ElementTextInputPropsSchema, diff --git a/packages/spec/src/ui/page.zod.ts b/packages/spec/src/ui/page.zod.ts index 25416b8488..e0c02000c8 100644 --- a/packages/spec/src/ui/page.zod.ts +++ b/packages/spec/src/ui/page.zod.ts @@ -75,7 +75,10 @@ export const PageComponentType = z.enum([ // only (the `type` union's open string arm still accepts any string); the // LOUD half of the retirement is `ElementFilterPropsSchema`'s retiredKey // tombstones, dispatched through the kept `ComponentPropsMap` row. - 'element:button', 'element:form', 'element:record_picker', 'element:text_input' + // `element:form` REMOVED (#9249, ADR-0049): the same shape one element over + // — retired at element grain, same mechanism; the tombstones' prescription + // names the live replacement, the object-bound `object-form` block (#7751). + 'element:button', 'element:record_picker', 'element:text_input' ]); /** From 1a4509c16f4eb479916b619429c5c18f556ca18a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Aug 2026 04:20:56 +0000 Subject: [PATCH 2/4] merge origin/main (os-regen artifacts taken from main; regeneration follows) --- content/docs/references/ui/component.mdx | 12 ++++++------ content/docs/references/ui/page.mdx | 3 ++- packages/spec/authorable-defaults/ui.json | 1 + packages/spec/authorable-surface/ui.json | 12 ++++++------ 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/content/docs/references/ui/component.mdx b/content/docs/references/ui/component.mdx index 092b0e508b..5269c1a6e3 100644 --- a/content/docs/references/ui/component.mdx +++ b/content/docs/references/ui/component.mdx @@ -77,12 +77,12 @@ const result = AIChatWindowProps.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **object** | `never` | optional | [REMOVED] `element:form` property `object` was removed in @objectstack/spec 17 (#9249, 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 no-renderer exclusion — "use the object-bound `object-form` block"), so every key on this element was a capability claim nothing kept. Delete the `element:form` component and use the object-bound `object-form` block instead (#7751) — it is rendered, designer-publishable, and carries the same intent (`objectName`, `fields`, `mode`, `submitText`). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **fields** | `never` | optional | [REMOVED] `element:form` property `fields` was removed in @objectstack/spec 17 (#9249, 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 no-renderer exclusion — "use the object-bound `object-form` block"), so every key on this element was a capability claim nothing kept. Delete the `element:form` component and use the object-bound `object-form` block instead (#7751) — it is rendered, designer-publishable, and carries the same intent (`objectName`, `fields`, `mode`, `submitText`). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **mode** | `never` | optional | [REMOVED] `element:form` property `mode` was removed in @objectstack/spec 17 (#9249, 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 no-renderer exclusion — "use the object-bound `object-form` block"), so every key on this element was a capability claim nothing kept. Delete the `element:form` component and use the object-bound `object-form` block instead (#7751) — it is rendered, designer-publishable, and carries the same intent (`objectName`, `fields`, `mode`, `submitText`). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **submitLabel** | `never` | optional | [REMOVED] `element:form` property `submitLabel` was removed in @objectstack/spec 17 (#9249, 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 no-renderer exclusion — "use the object-bound `object-form` block"), so every key on this element was a capability claim nothing kept. Delete the `element:form` component and use the object-bound `object-form` block instead (#7751) — it is rendered, designer-publishable, and carries the same intent (`objectName`, `fields`, `mode`, `submitText`). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **onSubmit** | `never` | optional | [REMOVED] `element:form` property `onSubmit` was removed in @objectstack/spec 17 (#9249, 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 no-renderer exclusion — "use the object-bound `object-form` block"), so every key on this element was a capability claim nothing kept. Delete the `element:form` component and use the object-bound `object-form` block instead (#7751) — it is rendered, designer-publishable, and carries the same intent (`objectName`, `fields`, `mode`, `submitText`). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | -| **aria** | `never` | optional | [REMOVED] `element:form` property `aria` was removed in @objectstack/spec 17 (#9249, 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 no-renderer exclusion — "use the object-bound `object-form` block"), so every key on this element was a capability claim nothing kept. Delete the `element:form` component and use the object-bound `object-form` block instead (#7751) — it is rendered, designer-publishable, and carries the same intent (`objectName`, `fields`, `mode`, `submitText`). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **object** | `string` | ✅ | Object for the form | +| **fields** | `string[]` | optional | Fields to display (defaults to all editable fields) | +| **mode** | `Enum<'create' \| 'edit'>` | optional (default: `"create"`) | Form mode | +| **submitLabel** | `string \| Record` | optional | Submit button label | +| **onSubmit** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Action expression on form submit (CEL) | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | --- diff --git a/content/docs/references/ui/page.mdx b/content/docs/references/ui/page.mdx index 766d0a69b5..22221d659f 100644 --- a/content/docs/references/ui/page.mdx +++ b/content/docs/references/ui/page.mdx @@ -105,7 +105,7 @@ Interface-level page configuration (Airtable parity) | 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' \| 'user:profile' \| 'ai:chat_window' \| 'ai:suggestion' \| 'element:text' \| 'element:number' \| 'element:image' \| 'element:divider' \| 'element:button' \| 'element:record_picker' \| 'element:text_input'> \| string` | ✅ | Component Type (Standard enum or custom string) | +| **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' \| 'user:profile' \| 'ai:chat_window' \| 'ai:suggestion' \| 'element:text' \| 'element:number' \| 'element:image' \| 'element:divider' \| 'element:button' \| 'element:form' \| 'element:record_picker' \| 'element:text_input'> \| string` | ✅ | Component Type (Standard enum or custom string) | | **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. | @@ -157,6 +157,7 @@ Interface-level page configuration (Airtable parity) * `element:image` * `element:divider` * `element:button` +* `element:form` * `element:record_picker` * `element:text_input` diff --git a/packages/spec/authorable-defaults/ui.json b/packages/spec/authorable-defaults/ui.json index d24f6db559..6a7edd4d6b 100644 --- a/packages/spec/authorable-defaults/ui.json +++ b/packages/spec/authorable-defaults/ui.json @@ -34,6 +34,7 @@ "ui/ElementButtonProps:iconPosition = \"left\"", "ui/ElementButtonProps:size = \"medium\"", "ui/ElementButtonProps:variant = \"primary\"", + "ui/ElementFormProps:mode = \"create\"", "ui/ElementImageProps:fit = \"cover\"", "ui/ElementMetadataViewerProps:detail = \"business\"", "ui/ElementTextInputProps:disabled = false", diff --git a/packages/spec/authorable-surface/ui.json b/packages/spec/authorable-surface/ui.json index 75623770a1..57d99fd848 100644 --- a/packages/spec/authorable-surface/ui.json +++ b/packages/spec/authorable-surface/ui.json @@ -361,12 +361,12 @@ "ui/ElementFilterProps:object [RETIRED]", "ui/ElementFilterProps:showSearch [RETIRED]", "ui/ElementFilterProps:targetVariable [RETIRED]", - "ui/ElementFormProps:aria [RETIRED]", - "ui/ElementFormProps:fields [RETIRED]", - "ui/ElementFormProps:mode [RETIRED]", - "ui/ElementFormProps:object [RETIRED]", - "ui/ElementFormProps:onSubmit [RETIRED]", - "ui/ElementFormProps:submitLabel [RETIRED]", + "ui/ElementFormProps:aria", + "ui/ElementFormProps:fields", + "ui/ElementFormProps:mode", + "ui/ElementFormProps:object", + "ui/ElementFormProps:onSubmit", + "ui/ElementFormProps:submitLabel", "ui/ElementImageProps:alt", "ui/ElementImageProps:aria", "ui/ElementImageProps:fit", From 712b269fc9cda30916e55263cb335d20ff44d42e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Aug 2026 04:37:22 +0000 Subject: [PATCH 3/4] chore(spec): re-take the element-grain verdict on today's refs and regenerate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The withdrawal audit found the WIP's measurement was never written down; the verdict is re-taken on today's merged refs (objectstack dd84ddd796, objectui 3ece13e33 — zero element:form readers, positive controls passing; cloud stays settled per the card's two recorded readings) and the six retired-key entries, the D2 conversion comment and the changeset now cite those refs. Regenerated: migration-registry concat, authorable surface/defaults, reference docs, openapi restore. --- .changeset/element-form-retired.md | 2 +- content/docs/references/ui/component.mdx | 12 ++++++------ content/docs/references/ui/page.mdx | 3 +-- packages/spec/authorable-defaults/ui.json | 1 - packages/spec/authorable-surface/ui.json | 12 ++++++------ packages/spec/src/conversions/registry.ts | 4 ++-- .../retired-keys/18.ui__ElementFormProps__aria.ts | 2 +- .../retired-keys/18.ui__ElementFormProps__fields.ts | 2 +- .../retired-keys/18.ui__ElementFormProps__mode.ts | 2 +- .../retired-keys/18.ui__ElementFormProps__object.ts | 2 +- .../18.ui__ElementFormProps__onSubmit.ts | 2 +- .../18.ui__ElementFormProps__submitLabel.ts | 2 +- packages/spec/src/migrations/registry.ts | 12 ++++++------ 13 files changed, 28 insertions(+), 30 deletions(-) diff --git a/.changeset/element-form-retired.md b/.changeset/element-form-retired.md index 3cf18d05c5..7c5c0ec9a7 100644 --- a/.changeset/element-form-retired.md +++ b/.changeset/element-form-retired.md @@ -12,7 +12,7 @@ registered under protocol major 18, where `os migrate meta` users will look). `element:form` never had a renderer or reader anywhere — the #9220 (`element:filter`) shape one element over, recorded by that card's own verdict -sweep. Measured at retirement (objectstack `c684d00cfc`, objectui `76ceb1e`; +sweep. Measured at retirement (objectstack `dd84ddd796`, objectui `3ece13e33`; cloud per the origin card's two recorded readings at `5f1bf23f` and `a11458b`, positive controls passing): objectui registers no renderer for it — its `renderers/basic/elements.tsx` header deferred the element to "owning plugins" diff --git a/content/docs/references/ui/component.mdx b/content/docs/references/ui/component.mdx index 5269c1a6e3..092b0e508b 100644 --- a/content/docs/references/ui/component.mdx +++ b/content/docs/references/ui/component.mdx @@ -77,12 +77,12 @@ const result = AIChatWindowProps.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **object** | `string` | ✅ | Object for the form | -| **fields** | `string[]` | optional | Fields to display (defaults to all editable fields) | -| **mode** | `Enum<'create' \| 'edit'>` | optional (default: `"create"`) | Form mode | -| **submitLabel** | `string \| Record` | optional | Submit button label | -| **onSubmit** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Action expression on form submit (CEL) | -| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +| **object** | `never` | optional | [REMOVED] `element:form` property `object` was removed in @objectstack/spec 17 (#9249, 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 no-renderer exclusion — "use the object-bound `object-form` block"), so every key on this element was a capability claim nothing kept. Delete the `element:form` component and use the object-bound `object-form` block instead (#7751) — it is rendered, designer-publishable, and carries the same intent (`objectName`, `fields`, `mode`, `submitText`). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **fields** | `never` | optional | [REMOVED] `element:form` property `fields` was removed in @objectstack/spec 17 (#9249, 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 no-renderer exclusion — "use the object-bound `object-form` block"), so every key on this element was a capability claim nothing kept. Delete the `element:form` component and use the object-bound `object-form` block instead (#7751) — it is rendered, designer-publishable, and carries the same intent (`objectName`, `fields`, `mode`, `submitText`). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **mode** | `never` | optional | [REMOVED] `element:form` property `mode` was removed in @objectstack/spec 17 (#9249, 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 no-renderer exclusion — "use the object-bound `object-form` block"), so every key on this element was a capability claim nothing kept. Delete the `element:form` component and use the object-bound `object-form` block instead (#7751) — it is rendered, designer-publishable, and carries the same intent (`objectName`, `fields`, `mode`, `submitText`). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **submitLabel** | `never` | optional | [REMOVED] `element:form` property `submitLabel` was removed in @objectstack/spec 17 (#9249, 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 no-renderer exclusion — "use the object-bound `object-form` block"), so every key on this element was a capability claim nothing kept. Delete the `element:form` component and use the object-bound `object-form` block instead (#7751) — it is rendered, designer-publishable, and carries the same intent (`objectName`, `fields`, `mode`, `submitText`). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **onSubmit** | `never` | optional | [REMOVED] `element:form` property `onSubmit` was removed in @objectstack/spec 17 (#9249, 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 no-renderer exclusion — "use the object-bound `object-form` block"), so every key on this element was a capability claim nothing kept. Delete the `element:form` component and use the object-bound `object-form` block instead (#7751) — it is rendered, designer-publishable, and carries the same intent (`objectName`, `fields`, `mode`, `submitText`). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | +| **aria** | `never` | optional | [REMOVED] `element:form` property `aria` was removed in @objectstack/spec 17 (#9249, 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 no-renderer exclusion — "use the object-bound `object-form` block"), so every key on this element was a capability claim nothing kept. Delete the `element:form` component and use the object-bound `object-form` block instead (#7751) — it is rendered, designer-publishable, and carries the same intent (`objectName`, `fields`, `mode`, `submitText`). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | --- diff --git a/content/docs/references/ui/page.mdx b/content/docs/references/ui/page.mdx index 22221d659f..766d0a69b5 100644 --- a/content/docs/references/ui/page.mdx +++ b/content/docs/references/ui/page.mdx @@ -105,7 +105,7 @@ Interface-level page configuration (Airtable parity) | 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' \| 'user:profile' \| 'ai:chat_window' \| 'ai:suggestion' \| 'element:text' \| 'element:number' \| 'element:image' \| 'element:divider' \| 'element:button' \| 'element:form' \| 'element:record_picker' \| 'element:text_input'> \| string` | ✅ | Component Type (Standard enum or custom string) | +| **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' \| 'user:profile' \| 'ai:chat_window' \| 'ai:suggestion' \| 'element:text' \| 'element:number' \| 'element:image' \| 'element:divider' \| 'element:button' \| 'element:record_picker' \| 'element:text_input'> \| string` | ✅ | Component Type (Standard enum or custom string) | | **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. | @@ -157,7 +157,6 @@ Interface-level page configuration (Airtable parity) * `element:image` * `element:divider` * `element:button` -* `element:form` * `element:record_picker` * `element:text_input` diff --git a/packages/spec/authorable-defaults/ui.json b/packages/spec/authorable-defaults/ui.json index 6a7edd4d6b..d24f6db559 100644 --- a/packages/spec/authorable-defaults/ui.json +++ b/packages/spec/authorable-defaults/ui.json @@ -34,7 +34,6 @@ "ui/ElementButtonProps:iconPosition = \"left\"", "ui/ElementButtonProps:size = \"medium\"", "ui/ElementButtonProps:variant = \"primary\"", - "ui/ElementFormProps:mode = \"create\"", "ui/ElementImageProps:fit = \"cover\"", "ui/ElementMetadataViewerProps:detail = \"business\"", "ui/ElementTextInputProps:disabled = false", diff --git a/packages/spec/authorable-surface/ui.json b/packages/spec/authorable-surface/ui.json index 57d99fd848..75623770a1 100644 --- a/packages/spec/authorable-surface/ui.json +++ b/packages/spec/authorable-surface/ui.json @@ -361,12 +361,12 @@ "ui/ElementFilterProps:object [RETIRED]", "ui/ElementFilterProps:showSearch [RETIRED]", "ui/ElementFilterProps:targetVariable [RETIRED]", - "ui/ElementFormProps:aria", - "ui/ElementFormProps:fields", - "ui/ElementFormProps:mode", - "ui/ElementFormProps:object", - "ui/ElementFormProps:onSubmit", - "ui/ElementFormProps:submitLabel", + "ui/ElementFormProps:aria [RETIRED]", + "ui/ElementFormProps:fields [RETIRED]", + "ui/ElementFormProps:mode [RETIRED]", + "ui/ElementFormProps:object [RETIRED]", + "ui/ElementFormProps:onSubmit [RETIRED]", + "ui/ElementFormProps:submitLabel [RETIRED]", "ui/ElementImageProps:alt", "ui/ElementImageProps:aria", "ui/ElementImageProps:fit", diff --git a/packages/spec/src/conversions/registry.ts b/packages/spec/src/conversions/registry.ts index 942427c824..b9384a0380 100644 --- a/packages/spec/src/conversions/registry.ts +++ b/packages/spec/src/conversions/registry.ts @@ -7033,8 +7033,8 @@ const elementFilterRemoved: MetadataConversion = { * enforce-or-remove at ELEMENT grain). * * The #9220 shape one element over, recorded by that card's own verdict sweep - * and re-measured at retirement time (objectstack @c684d00cfc, objectui - * @76ceb1e; cloud per the card's two recorded readings @5f1bf23f / @a11458b): + * and re-measured at retirement time (objectstack @dd84ddd796, objectui + * @3ece13e33; cloud per the card's two recorded readings @5f1bf23f / @a11458b): * NO renderer or behavior reader of `element:form` exists anywhere. objectui * registers no renderer (its `renderers/basic/elements.tsx` header deferred * the element to "owning plugins" that never materialized), Studio's designer diff --git a/packages/spec/src/migrations/entries/retired-keys/18.ui__ElementFormProps__aria.ts b/packages/spec/src/migrations/entries/retired-keys/18.ui__ElementFormProps__aria.ts index 710d11aff3..1fc79fb947 100644 --- a/packages/spec/src/migrations/entries/retired-keys/18.ui__ElementFormProps__aria.ts +++ b/packages/spec/src/migrations/entries/retired-keys/18.ui__ElementFormProps__aria.ts @@ -8,7 +8,7 @@ // no-renderer PALETTE_EXCLUSIONS entry naming the live replacement ("no // renderer — use the object-bound `object-form` block"), and the 2026-06 // page-liveness audit recorded it rendering "Unknown component type". Measured -// at retirement time (objectstack @c684d00cfc, objectui @76ceb1e; cloud per +// at retirement time (objectstack @dd84ddd796, objectui @3ece13e33; cloud per // the card's two recorded readings @5f1bf23f / @a11458b): zero production // readers of any `element:form` key — so every key, this one included, was a // capability claim nothing kept. Per-key retirement would have been the wrong diff --git a/packages/spec/src/migrations/entries/retired-keys/18.ui__ElementFormProps__fields.ts b/packages/spec/src/migrations/entries/retired-keys/18.ui__ElementFormProps__fields.ts index 41559c932c..dab0289613 100644 --- a/packages/spec/src/migrations/entries/retired-keys/18.ui__ElementFormProps__fields.ts +++ b/packages/spec/src/migrations/entries/retired-keys/18.ui__ElementFormProps__fields.ts @@ -8,7 +8,7 @@ // no-renderer PALETTE_EXCLUSIONS entry naming the live replacement ("no // renderer — use the object-bound `object-form` block"), and the 2026-06 // page-liveness audit recorded it rendering "Unknown component type". Measured -// at retirement time (objectstack @c684d00cfc, objectui @76ceb1e; cloud per +// at retirement time (objectstack @dd84ddd796, objectui @3ece13e33; cloud per // the card's two recorded readings @5f1bf23f / @a11458b): zero production // readers of any `element:form` key — so every key, this one included, was a // capability claim nothing kept. Per-key retirement would have been the wrong diff --git a/packages/spec/src/migrations/entries/retired-keys/18.ui__ElementFormProps__mode.ts b/packages/spec/src/migrations/entries/retired-keys/18.ui__ElementFormProps__mode.ts index 6934f4f8f3..3767b70bd3 100644 --- a/packages/spec/src/migrations/entries/retired-keys/18.ui__ElementFormProps__mode.ts +++ b/packages/spec/src/migrations/entries/retired-keys/18.ui__ElementFormProps__mode.ts @@ -8,7 +8,7 @@ // no-renderer PALETTE_EXCLUSIONS entry naming the live replacement ("no // renderer — use the object-bound `object-form` block"), and the 2026-06 // page-liveness audit recorded it rendering "Unknown component type". Measured -// at retirement time (objectstack @c684d00cfc, objectui @76ceb1e; cloud per +// at retirement time (objectstack @dd84ddd796, objectui @3ece13e33; cloud per // the card's two recorded readings @5f1bf23f / @a11458b): zero production // readers of any `element:form` key — so every key, this one included, was a // capability claim nothing kept. Per-key retirement would have been the wrong diff --git a/packages/spec/src/migrations/entries/retired-keys/18.ui__ElementFormProps__object.ts b/packages/spec/src/migrations/entries/retired-keys/18.ui__ElementFormProps__object.ts index 9314081fd8..1576c5e758 100644 --- a/packages/spec/src/migrations/entries/retired-keys/18.ui__ElementFormProps__object.ts +++ b/packages/spec/src/migrations/entries/retired-keys/18.ui__ElementFormProps__object.ts @@ -8,7 +8,7 @@ // no-renderer PALETTE_EXCLUSIONS entry naming the live replacement ("no // renderer — use the object-bound `object-form` block"), and the 2026-06 // page-liveness audit recorded it rendering "Unknown component type". Measured -// at retirement time (objectstack @c684d00cfc, objectui @76ceb1e; cloud per +// at retirement time (objectstack @dd84ddd796, objectui @3ece13e33; cloud per // the card's two recorded readings @5f1bf23f / @a11458b): zero production // readers of any `element:form` key — so every key, this one included, was a // capability claim nothing kept. Per-key retirement would have been the wrong diff --git a/packages/spec/src/migrations/entries/retired-keys/18.ui__ElementFormProps__onSubmit.ts b/packages/spec/src/migrations/entries/retired-keys/18.ui__ElementFormProps__onSubmit.ts index fb97f3a687..b79de289e0 100644 --- a/packages/spec/src/migrations/entries/retired-keys/18.ui__ElementFormProps__onSubmit.ts +++ b/packages/spec/src/migrations/entries/retired-keys/18.ui__ElementFormProps__onSubmit.ts @@ -8,7 +8,7 @@ // no-renderer PALETTE_EXCLUSIONS entry naming the live replacement ("no // renderer — use the object-bound `object-form` block"), and the 2026-06 // page-liveness audit recorded it rendering "Unknown component type". Measured -// at retirement time (objectstack @c684d00cfc, objectui @76ceb1e; cloud per +// at retirement time (objectstack @dd84ddd796, objectui @3ece13e33; cloud per // the card's two recorded readings @5f1bf23f / @a11458b): zero production // readers of any `element:form` key — so every key, this one included, was a // capability claim nothing kept. Per-key retirement would have been the wrong diff --git a/packages/spec/src/migrations/entries/retired-keys/18.ui__ElementFormProps__submitLabel.ts b/packages/spec/src/migrations/entries/retired-keys/18.ui__ElementFormProps__submitLabel.ts index 3f9b581e68..b5ca8df852 100644 --- a/packages/spec/src/migrations/entries/retired-keys/18.ui__ElementFormProps__submitLabel.ts +++ b/packages/spec/src/migrations/entries/retired-keys/18.ui__ElementFormProps__submitLabel.ts @@ -8,7 +8,7 @@ // no-renderer PALETTE_EXCLUSIONS entry naming the live replacement ("no // renderer — use the object-bound `object-form` block"), and the 2026-06 // page-liveness audit recorded it rendering "Unknown component type". Measured -// at retirement time (objectstack @c684d00cfc, objectui @76ceb1e; cloud per +// at retirement time (objectstack @dd84ddd796, objectui @3ece13e33; cloud per // the card's two recorded readings @5f1bf23f / @a11458b): zero production // readers of any `element:form` key — so every key, this one included, was a // capability claim nothing kept. Per-key retirement would have been the wrong diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index 9b5932f92d..dcefed1260 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -6726,7 +6726,7 @@ export const RETIRED_KEYS_BY_MAJOR: Readonly> // no-renderer PALETTE_EXCLUSIONS entry naming the live replacement ("no // renderer — use the object-bound `object-form` block"), and the 2026-06 // page-liveness audit recorded it rendering "Unknown component type". Measured - // at retirement time (objectstack @c684d00cfc, objectui @76ceb1e; cloud per + // at retirement time (objectstack @dd84ddd796, objectui @3ece13e33; cloud per // the card's two recorded readings @5f1bf23f / @a11458b): zero production // readers of any `element:form` key — so every key, this one included, was a // capability claim nothing kept. Per-key retirement would have been the wrong @@ -6748,7 +6748,7 @@ export const RETIRED_KEYS_BY_MAJOR: Readonly> // no-renderer PALETTE_EXCLUSIONS entry naming the live replacement ("no // renderer — use the object-bound `object-form` block"), and the 2026-06 // page-liveness audit recorded it rendering "Unknown component type". Measured - // at retirement time (objectstack @c684d00cfc, objectui @76ceb1e; cloud per + // at retirement time (objectstack @dd84ddd796, objectui @3ece13e33; cloud per // the card's two recorded readings @5f1bf23f / @a11458b): zero production // readers of any `element:form` key — so every key, this one included, was a // capability claim nothing kept. Per-key retirement would have been the wrong @@ -6770,7 +6770,7 @@ export const RETIRED_KEYS_BY_MAJOR: Readonly> // no-renderer PALETTE_EXCLUSIONS entry naming the live replacement ("no // renderer — use the object-bound `object-form` block"), and the 2026-06 // page-liveness audit recorded it rendering "Unknown component type". Measured - // at retirement time (objectstack @c684d00cfc, objectui @76ceb1e; cloud per + // at retirement time (objectstack @dd84ddd796, objectui @3ece13e33; cloud per // the card's two recorded readings @5f1bf23f / @a11458b): zero production // readers of any `element:form` key — so every key, this one included, was a // capability claim nothing kept. Per-key retirement would have been the wrong @@ -6792,7 +6792,7 @@ export const RETIRED_KEYS_BY_MAJOR: Readonly> // no-renderer PALETTE_EXCLUSIONS entry naming the live replacement ("no // renderer — use the object-bound `object-form` block"), and the 2026-06 // page-liveness audit recorded it rendering "Unknown component type". Measured - // at retirement time (objectstack @c684d00cfc, objectui @76ceb1e; cloud per + // at retirement time (objectstack @dd84ddd796, objectui @3ece13e33; cloud per // the card's two recorded readings @5f1bf23f / @a11458b): zero production // readers of any `element:form` key — so every key, this one included, was a // capability claim nothing kept. Per-key retirement would have been the wrong @@ -6814,7 +6814,7 @@ export const RETIRED_KEYS_BY_MAJOR: Readonly> // no-renderer PALETTE_EXCLUSIONS entry naming the live replacement ("no // renderer — use the object-bound `object-form` block"), and the 2026-06 // page-liveness audit recorded it rendering "Unknown component type". Measured - // at retirement time (objectstack @c684d00cfc, objectui @76ceb1e; cloud per + // at retirement time (objectstack @dd84ddd796, objectui @3ece13e33; cloud per // the card's two recorded readings @5f1bf23f / @a11458b): zero production // readers of any `element:form` key — so every key, this one included, was a // capability claim nothing kept. Per-key retirement would have been the wrong @@ -6836,7 +6836,7 @@ export const RETIRED_KEYS_BY_MAJOR: Readonly> // no-renderer PALETTE_EXCLUSIONS entry naming the live replacement ("no // renderer — use the object-bound `object-form` block"), and the 2026-06 // page-liveness audit recorded it rendering "Unknown component type". Measured - // at retirement time (objectstack @c684d00cfc, objectui @76ceb1e; cloud per + // at retirement time (objectstack @dd84ddd796, objectui @3ece13e33; cloud per // the card's two recorded readings @5f1bf23f / @a11458b): zero production // readers of any `element:form` key — so every key, this one included, was a // capability claim nothing kept. Per-key retirement would have been the wrong From 72f5cab986e68e4bfccb18c1b9072137429c04b9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Aug 2026 05:02:27 +0000 Subject: [PATCH 4/4] test(dogfood): delete the stranded onSubmit cover (#9249) Retiring element:form removed the last ExpressionInputSchema member the cel-ui row's 'submit predicates' half covered; the ADR-0060 ratchet correctly reported STALE covers. Deleted rather than re-pointed, the mapping.zod.ts:expression (#5552) way; the summary drops the dead half. --- .../qa/dogfood/test/expression-conformance.ledger.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/qa/dogfood/test/expression-conformance.ledger.ts b/packages/qa/dogfood/test/expression-conformance.ledger.ts index 069e9a4d94..d1eed0a584 100644 --- a/packages/qa/dogfood/test/expression-conformance.ledger.ts +++ b/packages/qa/dogfood/test/expression-conformance.ledger.ts @@ -128,7 +128,9 @@ export const EXPRESSION_SURFACE: ExprSurface[] = [ }, { id: 'cel-ui', - summary: 'UI visibility / routing / submit predicates', + // "+ submit predicates" left this summary with #9249: `element:form.onSubmit` + // was the one submit predicate here, and the whole element retired. + summary: 'UI visibility / routing predicates', dialect: 'cel', mode: 'interpret', state: 'enforced', failPolicy: 'fail-soft-log', enforcement: 'console (objectui) SchemaRenderer + server celEngine (interpret)', covers: [ @@ -148,7 +150,12 @@ export const EXPRESSION_SURFACE: ExprSurface[] = [ 'ui/view.zod.ts:condition', 'ui/view.zod.ts:visibleWhen', 'ui/view.zod.ts:visibleOn', - 'ui/component.zod.ts:onSubmit', + // `ui/component.zod.ts:onSubmit` (element:form's submit CEL) sat here + // until #9249 retired the whole `element:form` element under ADR-0049 + // (no renderer ever shipped, so nothing ever evaluated the predicate). + // The key is a retiredKey() tombstone now — no ExpressionInputSchema + // member left in source — so the cover is deleted rather than + // re-pointed, the mapping.zod.ts:expression (#5552) way. // Conditional tabs (framework#2606): `page:tabs` item-level visibility. // Canonical ADR-0089 `visibleWhen` from day one (no deprecated alias on // this new surface). Interpreted by the objectui page:tabs renderer to