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
81 changes: 81 additions & 0 deletions .changeset/6067-component-meta-derive-from-canonical.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
---
'@object-ui/core': minor
---

`ComponentMeta` at the registry is now DERIVED from the one declaration in
`@object-ui/types` instead of restating it, and `tags` / `description` reach the
registration surface (objectui#6067).

## The convergence

`packages/core/src/registry/Registry.ts` declared its own `ComponentMeta`: thirteen
keys, of which nine were restated from `@object-ui/types`' `base.ts`, four were
registry-only (`tier`, `namespace`, `skipFallback`, `labelling`), and `tags` /
`description` were **absent** — although both are declared on the canonical type and on
the `ComponentMetaSchema` zod mirror. Two of the three authorities agreed and the
registration surface did not, so those two keys were unwritable at exactly the
declaration most component registrations import. That is the same two-key delta
objectui#5893 had just closed inside `@object-ui/types`, arriving a third time on a
third declaration, and objectui#5671 had already made the identical move for the sibling
type `ComponentInput` in this very file.

It is now:

```ts
export type RegistryComponentMetaExtras = {
tier?: 'public' | 'internal';
namespace?: string;
skipFallback?: boolean;
labelling?: 'control' | 'group' | 'display';
};

export type ComponentMeta = CanonicalComponentMeta & RegistryComponentMetaExtras;
```

`RegistryComponentMetaExtras` is newly exported from `@object-ui/core`.

**What changes for a consumer: `tags` and `description` become writable on the registry's
`ComponentMeta`. Nothing narrows.** No key is removed, no key is renamed, and no key's
type changes, so no existing registration stops compiling — verified by type-checking all
37 workspace consumers of `@object-ui/core` (`pnpm --filter '...@object-ui/core'`), which
is why this is a widening rather than the contract break a rename would have been. All
four registry-only keys have live consumers, and they are still declared here.

This is `minor` under this repository's policy that its own breaking changes never declare
`major` (`scripts/check-changeset-no-major.mjs`); nothing here is breaking in any case.

## Converge rather than rename, and why the four keys did not move

The alternative dispositions were to rename the type so the name stops claiming a mirror,
or to move the four registry keys onto `@object-ui/types`' `ComponentMeta` and re-export
it outright the way objectui#5671 handled `ComponentInput`.

Renaming was rejected because it cannot be done without a break: `@object-ui/core` is
published, `ComponentMeta` is exported from it, and dropping the name would break every
external consumer — while keeping it as an alias would leave the mirror claim standing
under a second spelling, which fixes nothing.

Moving the four keys was rejected because `skipFallback` and `namespace` are registration
mechanics — they describe how the registry keys an entry, not what a component is — and
`@object-ui/types`' `ComponentMeta` is the general, plugin-facing, AI-facing type. The
extension keeps them where they are read, under their own named type, while the eleven
shared members exist in exactly one place and can no longer drift.

## Pinned by key set, not by assignability

Every member of both shapes is optional, so `extends` is mutually **true** across the
diverged pair — an assignability assertion is green on the defect and would not have
caught it. Measured on the emitted `.d.ts` of both packages, before and after:

| reading | before | after |
|---|---|---|
| `Core extends Canonical` | `true` | `true` |
| `Canonical extends Core` | `true` | `true` |
| `Exclude<keyof Canonical, keyof Core>` | `"tags" \| "description"` | `never` |
| `Exclude<keyof Core, keyof Canonical>` | the four registry keys | the four registry keys |

The new pin asserts the third row and names the fourth explicitly; the assignability pair
is kept beside it, labelled, as the control that shows what it cannot see. A source-level
assertion that the canonical members are not restated locally covers the remaining failure
mode — a member-identical copy, which every `keyof` comparison stays green on and which is
how the copy this replaces began.
64 changes: 46 additions & 18 deletions packages/core/src/registry/Registry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/

import type { ComponentInput } from '@object-ui/types';
import type { ComponentMeta as CanonicalComponentMeta } from '@object-ui/types';
import { PUBLIC_BLOCKS } from './public-blocks.js';

export type ComponentRenderer<T = any> = T;
Expand DownExpand Up@@ -34,10 +34,27 @@ export type ComponentRenderer<T = any> = T;
*/
export type { ComponentInput } from '@object-ui/types';

export type ComponentMeta = {
label?: string; // Display name in designer
icon?: string; // Icon name or svg string
category?: string; // Grouping category
/**
* The keys the REGISTRY adds on top of the one `ComponentMeta` declaration:
* registration mechanics (`tier` / `namespace` / `skipFallback`) and the
* host-labelling contract (`labelling`). None of the four has a counterpart on
* the general type in `@object-ui/types`, and none is being moved there —
* publishing registry mechanics on the general type was the alternative
* objectui#6067 weighed and rejected.
*
* They live in their OWN named type so that `ComponentMeta` below can be
* DERIVED from the canonical declaration instead of restating it. Until
* objectui#6067 this file carried a second, thirteen-key structural copy of
* the name: the nine shared members restated from `@object-ui/types`' `base.ts`,
* these four added here, and `tags` / `description` — declared on the canonical
* type and on the `ComponentMetaSchema` zod mirror — simply absent. That is
* objectui#4580's ruling coming true for the third type in a row: *a
* structural copy would reproduce the defect the moment either side moved.*
* objectui#5671 executed the same convergence for `ComponentInput` in this very
* file, and objectui#5893 closed the identical two-key delta inside
* `@object-ui/types`.
*/
export type RegistryComponentMetaExtras = {
/**
* Public contract tier (ADR-0080). `'public'` = part of the curated,
* type-checked, AI-facing block set (gets a strengthened contract, the
Expand DownExpand Up@@ -95,21 +112,32 @@ export type ComponentMeta = {
* unlabelled group.
*/
labelling?: 'control' | 'group' | 'display';
inputs?: ComponentInput[];
defaultProps?: Record<string, any>; // Default props when dropped
examples?: Record<string, any>; // Example configurations
isContainer?: boolean; // Whether the component can have children
resizable?: boolean; // Whether the component can be resized in the designer
resizeConstraints?: {
width?: boolean;
height?: boolean;
minWidth?: number;
maxWidth?: number;
minHeight?: number;
maxHeight?: number;
};
};

/**
* What a registration DECLARES about one component — the type every
* `ComponentRegistry.register` / `registerLazy` call is checked against.
*
* ONE declaration of the shared members: this is `@object-ui/types`'
* `ComponentMeta` (the declaration `ComponentMetaSchema` mirrors and the
* plugin-facing surface publishes) intersected with the registry-only keys
* above. The nine shared members are no longer restated here, so they cannot
* drift again, and `tags` / `description` now reach the registration surface —
* the two keys the divergence had made unwritable at the very declaration most
* component registrations import.
*
* NOTE for anyone pinning this: every member of both halves is OPTIONAL, so
* `extends` is mutually TRUE between the two shapes even when their key sets
* differ. Measured on the EMITTED `.d.ts` of both packages immediately before
* this convergence: `Core extends Canonical` and `Canonical extends Core` were
* BOTH `true` while `tags` / `description` were missing and four keys were
* extra. An assignability assertion is therefore a ghost here — it was green on
* the diverged tree. `__tests__/component-meta-derives-from-canonical.test.ts`
* compares `keyof` sets instead and keeps the assignability check beside it as
* the labelled control that shows why.
*/
export type ComponentMeta = CanonicalComponentMeta & RegistryComponentMetaExtras;

export type ComponentConfig<T = any> = ComponentMeta & {
type: string;
component: ComponentRenderer<T>;
Expand Down
Loading
Loading