Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
---
'@object-ui/types': minor
'@object-ui/core': patch
'@object-ui/components': patch
'@object-ui/layout': patch
'@object-ui/app-shell': patch
'@object-ui/plugin-ai': patch
'@object-ui/plugin-calendar': patch
'@object-ui/plugin-charts': patch
'@object-ui/plugin-chatbot': patch
'@object-ui/plugin-dashboard': patch
'@object-ui/plugin-designer': patch
'@object-ui/plugin-detail': patch
'@object-ui/plugin-editor': patch
'@object-ui/plugin-form': patch
'@object-ui/plugin-gantt': patch
'@object-ui/plugin-grid': patch
'@object-ui/plugin-kanban': patch
'@object-ui/plugin-list': patch
'@object-ui/plugin-map': patch
'@object-ui/plugin-markdown': patch
'@object-ui/plugin-report': patch
'@object-ui/plugin-timeline': patch
'@object-ui/plugin-tree': patch
'@object-ui/plugin-view': patch
---

**Breaking for authored metadata:** `ComponentInput.label`, `ComponentInput.defaultValue` and
`ComponentInput.advanced` are RETIRED on both faces (objectui#7493 item ① and objectui#7781;
maintainer ruling A of 2026-09-06, immediate, no deprecation window; ADR-0049 enforce-or-remove).
They are the three keys the manifest serializer does not forward, and nothing read them on any
publication or consumption path.

No manifest ever published them, so no consumer could ever have read them. `sdui-parser`'s
serializer (`packages/sdui-parser/src/index.ts`) forwards exactly six keys per input — `name`,
`type`, `required`, `enum`, `binding`, `description` — so a value authored under any of the three
never reached `sdui.manifest.json`, the generated JSX `.d.ts`, or a diagnostic; its boundary type
has no slot for them; the registry's data-source seam reads `name` only; and neither the designer
nor the app-shell inspectors consult registry `inputs` at all. A structural census over every
`inputs:` array in the repository (re-measured on this change's merge-base, `name` 951 and `type`
951 as the controls) counted the writes: `label` 908, `defaultValue` 245, `advanced` 9 — written on
nearly every registration, read by nothing.

FROM → TO, per key — all three **TOMBSTONED, not removed**, because the route was measured on
the built face before it was chosen: `ComponentInputSchema` is a non-strict `z.object`, and an
undeclared key parses GREEN and is silently STRIPPED, so a deletion would have swallowed 1,162
authored values in silence. The tombstone is what makes the refusal loud and by name.

- `label?: string` → `label?: never` on the interface, `retirementTombstone()` on the Zod mirror.
Migration: delete the key. An input is identified by its `name` on every path that reaches it;
nothing ever rendered a label for it.
- `defaultValue?: any` → `defaultValue?: never` / `retirementTombstone()`. Migration: delete the
key. The renderer's own fallback read IS the default; tell the author about it in `description`,
which IS published. (Tightening the type to `unknown` was ruled out: it closes no error class,
since nothing reads the value.)
- `advanced?: boolean` → `advanced?: never` / `retirementTombstone()`. Migration: delete the key.
No designer surface ever hid an "advanced" input; there is nothing to write instead.

The retirement kit: `?: never` on `ComponentInput` (`packages/types/src/base.ts`), so authoring one
is a `tsc` error at the registration site; `retirementTombstone()` on `ComponentInputSchema`
(`packages/types/src/zod/base.zod.ts`), so an authored value is REFUSED at parse time with
`code: 'invalid_type'`, the key named in the issue `path`, and the migration note as the message
(one string, both channels). Pinned in
`packages/types/src/__tests__/component-input-retired-keys-7493.test.ts`, which also holds a
tree-scoped absence census over every `inputs:` array under `packages/**` and `apps/**`.

Accept-set change, stated plainly for reviewers: a document that sets any of the three keys on a
`ComponentInput` used to parse GREEN (the value was then dropped by the serializer) and now parses
RED. Every in-repo authoring site — 1,199 keys across 110 registration files, the three standalone
`ComponentInput[]` arrays and the two named input arrays `tsc` found included — is deleted in the same change, as the ruling's split rule
requires; the `WidgetRegistry` seam no longer copies the widget-manifest values onto the synthesized
`ComponentInput` (they fed nothing), and the data-source declaration `ELEMENT_DATA_SOURCE_INPUT`
drops its `label`. The patch entries on the other packages record exactly that: their registrations
stop authoring inert keys, with no runtime or published-manifest change.

The nine test files that read `defaultValue` off a registration were re-pinned against the
renderer's ACTUAL default (its own fallback read, or the `defaultProps` it ships) instead of the
declaration that went away; two assertions that only restated the shadow default were dropped with
the reason on the line.

The in-repo zero is what was measured. Whether anything OUTSIDE this repository writes these keys
is not measurable from here (the objectui#5674 limit); converting such a write from a silent drop
into a named refusal is exactly what the tombstones buy. `WidgetInput`'s own `label` /
`defaultValue` / `advanced` (the widget-manifest face) stay declared and writable — nothing has
ruled on that face; that it now has no reader either is recorded as objectui#7911.
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,9 @@ const sampleFor = (input: any): unknown => {
if (input.name === 'details') {
return [{ childObject: PROBE_OBJECT, title: 'Probe Detail' }];
}
if (input.defaultValue !== undefined) return input.defaultValue;
// (`ComponentInput.defaultValue` is an ADR-0049 tombstone since objectui#7493 —
// no registration declares a default any more, so the sample is decided by the
// declared TYPE below, as it already was for every input without one.)
switch (input.type) {
case 'number':
return 1;
Expand Down
28 changes: 27 additions & 1 deletion apps/console/src/__tests__/record-block-record-reach.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -245,12 +245,38 @@ const SAMPLE_BY_INPUT: Readonly<Record<string, unknown>> = {
// affordance is configured against something that exists rather than at a
// dangling name.
add: { picker: { object: PROBE_CHILD_OBJECT } },
// The SIXTH instance arrived with objectui#7493, which retired
// `ComponentInput.defaultValue`: until then `sampleFor` seeded these six from
// the registration's declared default, which happened to be the renderer's
// own fallback and the one value under which the probe is askable. The
// declared default is gone (nothing but this sampler ever read it), so each
// renderer default is restated HERE, next to the read it mirrors:
// - `record:related_list` reads `relationshipValueField || 'id'`; the
// generic `'x'` names a parent field the record does not carry, so the
// list holds its fetch (by design) and the record cannot reach the output.
// - `record:details` passes `showHeader ?? false`; with the header chrome ON
// the same record renders differently twice and the stability control
// (rightly) refuses to grade the probe. The header is not what this probe
// asks about.
// - `record:quick_actions` reads `location || 'record_header'`,
// `align || 'end'`, `variant || 'default'`, `size || 'sm'`; the
// probe action is declared at the header location, and the generic
// samples (`'x'` for the two strings, the enum's first arm for the
// other two) point the bar at a location with no actions.
relationshipValueField: 'id',
showHeader: false,
location: 'record_header',
align: 'end',
variant: 'default',
size: 'sm',
};

/** Fill one declared input. */
const sampleFor = (input: any): unknown => {
if (input.name in SAMPLE_BY_INPUT) return SAMPLE_BY_INPUT[input.name];
if (input.defaultValue !== undefined) return input.defaultValue;
// (`ComponentInput.defaultValue` is an ADR-0049 tombstone since objectui#7493 —
// no registration declares a default any more, so the sample is decided by the
// declared TYPE below, as it already was for every input without one.)
switch (input.type) {
case 'number': return 1;
case 'boolean': return true;
Expand Down
2 changes: 1 addition & 1 deletion content/docs/guide/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ ComponentRegistry.register('my-widget', MyWidgetComponent, {
category: 'Custom',
icon: 'box',
inputs: [
{ name: 'title', type: 'string', label: 'Title' }
{ name: 'title', type: 'string' }
]
})
```
Expand Down
14 changes: 7 additions & 7 deletions content/docs/guide/component-registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@ ComponentRegistry.register('my-component', MyComponent, {
category: 'Custom',
icon: 'component-icon',
inputs: [
{ name: 'title', type: 'string', label: 'Title' },
{ name: 'content', type: 'string', label: 'Content' }
{ name: 'title', type: 'string' },
{ name: 'content', type: 'string' }
]
})
```
Expand Down Expand Up @@ -434,11 +434,11 @@ ComponentRegistry.register('rating', RatingComponent, {
category: 'Form',
labelling: 'group',
inputs: [
{ name: 'name', type: 'string', label: 'Name', required: true },
{ name: 'label', type: 'string', label: 'Label' },
{ name: 'maxStars', type: 'number', label: 'Max stars', defaultValue: 5 },
{ name: 'required', type: 'boolean', label: 'Required' },
{ name: 'disabled', type: 'boolean', label: 'Disabled' }
{ name: 'name', type: 'string', required: true },
{ name: 'label', type: 'string' },
{ name: 'maxStars', type: 'number', description: 'Defaults to 5 — the renderer\'s own fallback' },
{ name: 'required', type: 'boolean' },
{ name: 'disabled', type: 'boolean' }
]
})

Expand Down
13 changes: 6 additions & 7 deletions content/docs/guide/plugin-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,8 @@ ComponentRegistry.register('board', BoardRenderer, {
label: 'Board View',
category: 'plugin',
inputs: [
{ name: 'columns', type: 'array', label: 'Columns', required: true },
{ name: 'items', type: 'array', label: 'Items', required: true },
{ name: 'columns', type: 'array', required: true },
{ name: 'items', type: 'array', required: true },
],
defaultProps: {
columns: [
Expand Down Expand Up @@ -366,20 +366,19 @@ export interface BoardSchema extends BaseSchema {
}
```

Declare `ComponentInput` entries when registering so the visual designer can offer a property panel:
Declare `ComponentInput` entries when registering: they are what the published manifest (`sdui.manifest.json`) and the JSX-page compiler's diagnostics read. Each entry carries the six keys the manifest forwards — `name`, `type`, `required`, `enum`, `binding`, `description`; a default belongs in the renderer's own fallback read and, for the author, in `description` (`label`, `defaultValue` and `advanced` are retired keys — nothing ever read them):

<!-- doc-snippet: fragment — continues the board example: ComponentRegistry and BoardRenderer both come from step 3's src/index.tsx; this block shows only the inputs metadata -->
```tsx
ComponentRegistry.register('board', BoardRenderer, {
inputs: [
{ name: 'columns', type: 'array', label: 'Columns', required: true },
{ name: 'items', type: 'array', label: 'Items', required: true },
{ name: 'columns', type: 'array', required: true },
{ name: 'items', type: 'array', required: true },
{
name: 'layout',
type: 'enum',
label: 'Layout',
enum: ['horizontal', 'vertical'],
defaultValue: 'horizontal',
description: 'Defaults to "horizontal" — the renderer\'s own fallback',
},
],
});
Expand Down
2 changes: 1 addition & 1 deletion packages/app-shell/src/views/record-approvals-renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ ComponentRegistry.register('approvals', RecordApprovalsRenderer, {
category: 'record',
label: 'Approvals',
icon: 'Stamp',
inputs: [{ name: 'className', type: 'string', label: 'CSS Class' }],
inputs: [{ name: 'className', type: 'string' }],
});

export default RecordApprovalsRenderer;
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ ComponentRegistry.register('attachments', RecordAttachmentsRenderer, {
category: 'record',
label: 'Attachments',
icon: 'Paperclip',
inputs: [{ name: 'className', type: 'string', label: 'CSS Class' }],
inputs: [{ name: 'className', type: 'string' }],
});

export default RecordAttachmentsRenderer;
Original file line number Diff line number Diff line change
Expand Up @@ -68,43 +68,51 @@ import '../renderers/basic/icon';
// extend the gate to the whole population — ⛔ not to restore a pin over one
// registration.
//
// ── What this file still asserts, and why that half survives the same argument
// The coupling claim below is NOT a claim about lucide's vocabulary. It needs no
// external record, no copied tokeniser and no consumer to be well-formed: it
// says two declarations in ONE registration agree with each other. It therefore
// cannot drift the way the membership half did, costs nothing to keep, and
// becomes correct the instant any consumer appears.
// ── The COUPLING half was retired at objectui#7493 ──────────────────────────
// It asserted that the registration's `icon` meta and the glyph input's
// `defaultValue` named the same glyph. `ComponentInput.defaultValue` is an
// ADR-0049 retirement tombstone since objectui#7493 (with `label` and
// `advanced`, objectui#7781): the manifest serializer never forwarded it and
// no consumer of `ComponentMeta.inputs` — no designer, no palette, no renderer
// — ever read it, so the "second spelling" was a shadow value only this pin
// compared. There is no renderer default to re-pin it against either: an
// `icon` node with no `icon` name renders the PLACEHOLDER branch and warns
// (objectui#5631), by design — the renderer has no fallback glyph.
//
// ── What this file still asserts
// The registration's `icon` meta — the ONE declared spelling left — is a real
// icon name, and the `icon` input is still declared (it is what the manifest
// publishes for the key). Both are preconditions the old coupling test carried;
// they survive because the ledger in
// `scripts/__tests__/check-lucide-icon-record-names.test.ts` keeps this file as
// the record of the objectui#5936 retirement, and because a registration whose
// palette glyph silently went blank is the objectui#5622 defect's other half.
// ---------------------------------------------------------------------------

const meta = ComponentRegistry.getMeta('icon', 'ui');
const glyphInput = meta?.inputs?.find(input => input.name === 'icon');

/** Both declared spellings, each labelled by the surface it drives. */
const DECLARED_DEFAULTS: Array<[string, string | undefined]> = [
['registration `icon` (the palette entry glyph)', meta?.icon],
['`icon` input `defaultValue` (what a dropped `icon` renders)', glyphInput?.defaultValue as string | undefined],
];

describe('the `ui:icon` renderer\'s declared spellings agree (objectui#5622)', () => {
it('both declared spellings were actually found — the precondition', () => {
// Load-bearing, not ceremony: the assertion below compares two values that
// are BOTH `undefined` if the registry read comes back empty, and
// `undefined === undefined` passes. Without this the coupling check goes
// vacuously green on a registration that no longer exists.
describe('the `ui:icon` registration\'s one declared glyph spelling (objectui#5622, objectui#7493)', () => {
it('the registration and its `icon` input were actually found — the precondition', () => {
// Load-bearing, not ceremony: `typeof undefined` is a string too, so the
// glyph assertion below needs the registration to exist first.
expect(meta, '`ui:icon` is not registered — the import above no longer registers it.').toBeDefined();
expect(
glyphInput,
'the `icon` input is gone from the `ui:icon` registration — fix the reader or the registration.',
).toBeDefined();
for (const [surface, spelling] of DECLARED_DEFAULTS) {
expect(typeof spelling, `${surface} declares no icon name at all`).toBe('string');
}
});

it('keeps the palette glyph and the dropped default the same name', () => {
// The defect was one name in two places; the repair is only correct if they
// stay one name. Split them and the palette advertises a glyph the dropped
// component does not render.
expect(meta?.icon).toBe(glyphInput?.defaultValue);
it('declares the palette glyph as a non-empty icon name', () => {
expect(typeof meta?.icon, 'registration `icon` (the palette entry glyph) declares no icon name at all').toBe('string');
expect(meta?.icon).not.toBe('');
});

it('the `icon` input carries no declared default — the tombstone, read off the registry', () => {
// Not `toBeUndefined()` on the property: that would be vacuously green on a
// missing input too (the precondition above guards it), and the point is
// that the key is ABSENT from what the registration publishes, not that its
// value happens to be undefined.
expect(glyphInput).not.toHaveProperty('defaultValue');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -209,17 +209,13 @@ describe('element:record_picker — registry inputs vs @objectstack/spec', () =>
expect(description).toMatch(/\$filter/);
});

it('carries no `defaultValue` on the filter entry', () => {
// A default here would pre-fill every picker in the designer with a filter
// the renderer has no opinion about — and a filter's default is not "empty
// object", it is "no filter at all", which `undefined` already is. The spec
// declares no default either.
//
// Existence asserted first: `input('filter')?.defaultValue` is also
// `undefined` when the input is GONE, so without this line the check would
// pass most loudly in the one case it is supposed to notice.
it('declares no default for `filter` — the spec parses none in', () => {
// A filter's default is not "empty object", it is "no filter at all", which
// `undefined` already is, and the spec declares no default. (This used to
// also assert the registration carried no `ComponentInput.defaultValue`;
// that key is an ADR-0049 tombstone since objectui#7493, so the spec's
// parse is the one channel a default could reach an author through.)
expect(input('filter')).toBeDefined();
expect(input('filter')?.defaultValue).toBeUndefined();
expect(
ElementRecordPickerPropsSchema.safeParse({ object: 'account' }).data,
).not.toHaveProperty('filter');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,17 +188,13 @@ describe('element:text_input — registry inputs vs @objectstack/spec', () => {
expect(description).toMatch(/empty/i);
});

it('carries no `defaultValue` OF ITS OWN on the defaultValue entry', () => {
// A `ComponentInput.defaultValue` on this input would publish a default for
// the default — the designer would pre-fill a seed value the renderer has no
// opinion about, and every text input in the gallery would come up carrying
// it. The spec declares no default here either.
//
// Existence asserted first: `input('defaultValue')?.defaultValue` is also
// `undefined` when the input is GONE, so without this line the check would
// pass most loudly in the one case it is supposed to notice.
it('declares no default OF ITS OWN for the defaultValue entry — the spec parses none in', () => {
// A default for the default would seed every text input in the gallery with
// a value the renderer has no opinion about; the spec declares none. (This
// used to also assert the registration carried no `ComponentInput.defaultValue`;
// that key is an ADR-0049 tombstone since objectui#7493, so the spec's parse
// is the one channel a default could reach an author through.)
expect(input('defaultValue')).toBeDefined();
expect(input('defaultValue')?.defaultValue).toBeUndefined();
expect(ElementTextInputPropsSchema.safeParse({}).data).not.toHaveProperty('defaultValue');
});
});
Expand Down
Loading
Loading