|
| 1 | +--- |
| 2 | +"@objectstack/spec": minor |
| 3 | +--- |
| 4 | + |
| 5 | +feat(spec): retire the `element:form` element at element grain (#9249, ADR-0049) |
| 6 | + |
| 7 | +<!-- adr-0087: registered element-form-removed --> |
| 8 | + |
| 9 | +**BREAKING** accept-set narrowing, landing after the v17.0.0 cut (the lockstep |
| 10 | +launch-window convention ships it as `minor`; the migration prescription is |
| 11 | +registered under protocol major 18, where `os migrate meta` users will look). |
| 12 | + |
| 13 | +`element:form` never had a renderer or reader anywhere — the #9220 |
| 14 | +(`element:filter`) shape one element over, recorded by that card's own verdict |
| 15 | +sweep. Measured at retirement (objectstack `dd84ddd796`, objectui `3ece13e33`; |
| 16 | +cloud per the origin card's two recorded readings at `5f1bf23f` and `a11458b`, |
| 17 | +positive controls passing): objectui registers no renderer for it — its |
| 18 | +`renderers/basic/elements.tsx` header deferred the element to "owning plugins" |
| 19 | +that never materialized — and Studio's designer palette carries it as a |
| 20 | +no-renderer `PALETTE_EXCLUSIONS` entry that names the live replacement ("no |
| 21 | +renderer — use the object-bound `object-form` block"). The 2026-06 |
| 22 | +page-liveness audit already recorded it rendering "Unknown component type". So |
| 23 | +the full form contract `ElementFormPropsSchema` declared — `object`, `fields`, |
| 24 | +`mode`, `submitLabel`, `onSubmit` (CEL), `aria` — was a capability claim |
| 25 | +nothing kept. |
| 26 | + |
| 27 | +**What is refused:** every authored `element:form` key. All six are |
| 28 | +`retiredKey()` tombstones, so authoring one is a `tsc` error and a parse error |
| 29 | +carrying the prescription (fully-qualified key, why the element is dead, the |
| 30 | +replacement, the `os migrate meta` pointer), dispatched through the KEPT |
| 31 | +`ComponentPropsMap` row — deleting the row would demote the type to an |
| 32 | +unregistered custom string the #5068 props gate deliberately skips. |
| 33 | + |
| 34 | +**What stays accepted:** a bare `element:form` node with empty `properties` |
| 35 | +(the migrated shape — the open `type` union accepts any string, and deleting |
| 36 | +authored page nodes is a layout decision a mechanical conversion must not |
| 37 | +make). It renders nothing, exactly as it always did. |
| 38 | + |
| 39 | +The retirement kit: |
| 40 | + |
| 41 | +- `retiredKey()` tombstones for all six keys at the schema |
| 42 | + (`packages/spec/src/ui/component.zod.ts`); the `PageComponentType` enum |
| 43 | + drops the value (de-advertisement — the open string arm still accepts it) |
| 44 | +- ADR-0087 registration: six retired-key entries |
| 45 | + `ui/ElementFormProps:{aria,fields,mode,object,onSubmit,submitLabel}` and the |
| 46 | + D2 conversion `element-form-removed` (protocol 18), wired into the step-18 |
| 47 | + chain — `os migrate meta --from 17` strips all six keys from authored |
| 48 | + `element:form` blocks (pure lossless deletes; none ever had an effect to |
| 49 | + lose) and leaves the bare node |
| 50 | +- pin tests (`component.test.ts` — refusal pins assert the prescription per |
| 51 | + key; a positive pin parses the bare migrated node clean and asserts nothing |
| 52 | + materializes; the kept-map-row pin flips from parse to refusal) |
| 53 | +- `packages/lint`'s `COMPONENT_FIELD_SPECS` entry drops (its own changeset); |
| 54 | + the component-translation `submitLabel` describes stop naming `element:form` |
| 55 | + as carrier (the orphaned-key decision is #10926, out of this card's scope) |
| 56 | +- generated baselines/docs follow the schema (authorable surface/defaults, |
| 57 | + spec-changes, upgrade guide, reference docs) |
| 58 | +- objectui's `elements.tsx` comment and Studio palette-exclusion lines are |
| 59 | + cross-repo and already queued under objectui#4935 |
| 60 | + |
| 61 | +## FROM → TO |
| 62 | + |
| 63 | +```ts |
| 64 | +// before — parsed green; nothing anywhere rendered it, so the page showed |
| 65 | +// "Unknown component type" where the author expected a form |
| 66 | +{ |
| 67 | + type: 'element:form', |
| 68 | + properties: { |
| 69 | + object: 'lead', |
| 70 | + fields: ['name', 'email'], |
| 71 | + mode: 'create', |
| 72 | + submitLabel: 'Create Lead', |
| 73 | + }, |
| 74 | +} |
| 75 | + |
| 76 | +// after — use the object-bound `object-form` block (#7751): rendered, |
| 77 | +// designer-publishable, same intent (`objectName`, `fields`, `mode`, |
| 78 | +// `submitText`) |
| 79 | +{ |
| 80 | + type: 'object-form', |
| 81 | + properties: { |
| 82 | + objectName: 'lead', |
| 83 | + fields: ['name', 'email'], |
| 84 | + mode: 'create', |
| 85 | + submitText: 'Create Lead', |
| 86 | + }, |
| 87 | +} |
| 88 | +``` |
0 commit comments