diff --git a/.changeset/6298-componentconfig-one-authority.md b/.changeset/6298-componentconfig-one-authority.md new file mode 100644 index 0000000000..21d49637bb --- /dev/null +++ b/.changeset/6298-componentconfig-one-authority.md @@ -0,0 +1,32 @@ +--- +'@object-ui/types': minor +'@object-ui/core': minor +--- + +`ComponentConfig` now has one authority: `@object-ui/types` declares it, `@object-ui/core` re-exports it + +`@object-ui/types` and `@object-ui/core` each published a declaration of +`ComponentConfig`, so an auto-import picked between two different types by +alphabetical order. After the `ComponentMeta` convergence the remaining +difference was genericity and the `component` slot: `@object-ui/types`' was +non-generic with `component: any`, core's was `` with +`component: ComponentRenderer`. + +`@object-ui/types`' declaration gains that type parameter, **defaulted**, so +every existing spelling keeps its meaning exactly — bare `ComponentConfig` is +`ComponentConfig`, whose `component` is `any`, as before. `@object-ui/core` +re-exports it instead of declaring its own. + +The registry-only keys (`tier`, `namespace`, `skipFallback`, `labelling`, +`deprecated`) were not dropped: they moved to a named extension, +`RegistryComponentConfig`, which is what `Registry.getConfig`, +`getAllConfigs` and `getNamespaceComponents` return. Those return values are +type-identical to what they returned before, so every read path is unchanged. + +**Breaking:** a consumer that imports `ComponentConfig` from `@object-ui/core` +*and* touches one of those five registry-only keys through that annotation must +switch the annotation to `RegistryComponentConfig` — the name `ComponentConfig` +no longer carries them there. Filed `minor` rather than `major` per AGENTS.md's +versioning policy: objectui's own breaking changes ship as `minor` with the break +spelled out here, because the whole publishable set is one changeset `fixed` group +pinned to `@objectstack`'s major. diff --git a/content/docs/guide/plugin-development.md b/content/docs/guide/plugin-development.md index 4b29340e55..cb43c4a7bb 100644 --- a/content/docs/guide/plugin-development.md +++ b/content/docs/guide/plugin-development.md @@ -348,7 +348,7 @@ import { ComponentRegistry } from '@object-ui/core'; ComponentRegistry.has('board'); // boolean ComponentRegistry.getAllTypes(); // string[] -ComponentRegistry.getNamespaceComponents('plugin-board'); // ComponentConfig[] +ComponentRegistry.getNamespaceComponents('plugin-board'); // RegistryComponentConfig[] ``` ## Plugin Configuration & Schema Types diff --git a/packages/core/src/registry/Registry.ts b/packages/core/src/registry/Registry.ts index d3720b838f..944093f2bb 100644 --- a/packages/core/src/registry/Registry.ts +++ b/packages/core/src/registry/Registry.ts @@ -7,12 +7,38 @@ */ import type { ComponentMeta as CanonicalComponentMeta } from '@object-ui/types'; +// Deliberately a SECOND import statement from the same module rather than a +// widening of the line above: `__tests__/component-meta-derives-from-canonical.test.ts` +// pins that line as an exact string (objectui#6067's source-identity assertion), +// so adding a specifier to it would red a pin that has nothing to do with this +// name. Two `import type` lines from one module is legal and costs nothing — +// type imports are erased. +import type { ComponentConfig } from '@object-ui/types'; import { ELEMENT_DATA_SOURCE_INPUT, isElementDataSourceBlock, } from '../data-scope/element-data-source.js'; import { PUBLIC_BLOCKS } from './public-blocks.js'; +/** + * The renderer a registration carries — the IDENTITY alias, deliberately. + * + * ⚠️ Load-bearing for objectui#6298, which is why it is documented rather than + * left as a bare line. `ComponentRenderer` resolves to `T` and contributes + * NO type information: that is the entire reason `@object-ui/types` can declare + * the one `ComponentConfig` with `component: T` and mean exactly what this + * package used to mean by `component: ComponentRenderer`, WITHOUT + * `@object-ui/types` needing to reach this declaration. It could not have + * reached it: `@object-ui/types` is the bottom layer (`packages/types/package.json` + * depends on `@objectstack/spec` and `zod` only) and this package depends on it, + * so an edge in that direction would be a cycle. + * + * The alias survives as the NAME this package's own renderer-shaped positions + * are spelled with ({@link withElementDataSourceInput}, {@link PublicComponentConfig}). + * If it is ever given real content, `component: T` over in `@object-ui/types` + * stops being the same slot — `__tests__/component-config-single-declaration.test.ts` + * asserts the identity so that change cannot pass unnoticed. + */ export type ComponentRenderer = T; /** @@ -233,16 +259,71 @@ export type RegistryComponentMetaExtras = { */ export type ComponentMeta = CanonicalComponentMeta & RegistryComponentMetaExtras; -export type ComponentConfig = ComponentMeta & { - type: string; - component: ComponentRenderer; -}; +/** + * ONE authority for `ComponentConfig` (objectui#6298) — this package RE-EXPORTS + * `@object-ui/types`' declaration instead of declaring a second one, the same + * disposition objectui#5671 gave `ComponentInput` a few lines above and + * objectui#4580 ruled for the whole family: *a structural copy would reproduce + * the defect the moment either side moved.* + * + * ## What was wrong + * + * Both spellings were PUBLISHED — `@object-ui/types`' `src/index.ts` exports its + * one, and this file reaches `@object-ui/core`'s public entry through + * `src/index.ts`'s `export * from './registry/Registry.js'`. An IDE auto-import + * therefore picked between two different types by alphabetical order. After + * objectui#6067 / PR #6297 single-sourced the `ComponentMeta` half, what still + * differed was GENERICITY AND THE `component` SLOT: `@object-ui/types`' was + * non-generic with `component: any`, this one was `` with + * `component: ComponentRenderer`. + * + * ⚠️ Measured on the EMITTED `.d.ts` of both packages immediately before this + * convergence, `Exact` — mutual assignability — read + * `true` on the DIVERGED pair, because `component: any` absorbs everything and + * every other member is optional. An assignability assertion is a GHOST here, + * exactly as `__tests__/component-meta-derives-from-canonical.test.ts` records + * for the sibling type. The readings that actually moved were "is + * `@object-ui/types`' declaration generic" (`TS2315: Type 'ComponentConfig' is + * not generic` before, no error after) and the symmetric key-set difference. + * + * A re-export is not a second authority — `scripts/__tests__/one-authority-per-exported-name-6273.test.ts` + * counts declarations and ALIASING re-exports, never `export type { X } from …` + * — which is why this convergence takes `ComponentConfig` off that gate's + * `KNOWN_COLLISIONS` baseline. Deriving a new declaration here instead would + * NOT have: `ComponentMeta` was converged that way by PR #6297 and is still a + * row on that baseline today. + */ +export type { ComponentConfig } from '@object-ui/types'; + +/** + * What the registry actually STORES and hands back — `ComponentConfig` plus the + * registry-only keys, as a NAMED extension (objectui#6298). + * + * This is type-identical to the `ComponentConfig` this file used to declare: + * `CanonicalComponentMeta & RegistryComponentMetaExtras & { type; component }`, + * reached from the other side. The two halves are named rather than restated — + * one declaration for the shared members ({@link ComponentConfig}, in + * `@object-ui/types`), a named extension for the rest + * ({@link RegistryComponentMetaExtras}) — which is the shape PR #6297 gave + * {@link ComponentMeta}. + * + * It exists because the extras are NOT optional decoration on a registry entry: + * {@link Registry.getNamespaceComponents} filters on `config.namespace`, and + * `tier` / `labelling` / `deprecated` are read off registrations elsewhere. A + * bare re-export as the entry type would have silently dropped them. + * + * ⚠️ `ComponentConfig` remains the AUTHORING vocabulary and the general name; + * registrations are checked against {@link ComponentMeta}, never against this. + * Nothing writes a `RegistryComponentConfig` literal — the registry builds them. + */ +export type RegistryComponentConfig = ComponentConfig & + RegistryComponentMetaExtras; /** * A CONTRACT-surface entry (ADR-0080), as returned by * {@link Registry.getPublicConfigs}. * - * Same shape as {@link ComponentConfig} except `component` is absent while the + * Same shape as {@link RegistryComponentConfig} except `component` is absent while the * entry is still a pending `registerLazy` stub: the plugin module has not been * imported yet, so there is no renderer to hand out. Consumers render such an * entry through `SchemaRenderer`, which triggers the loader and shows a @@ -314,7 +395,7 @@ export function withElementDataSourceInput( } export class Registry { - private components = new Map>(); + private components = new Map>(); private lazyEntries = new Map(); /** * Notifies subscribers that the registry has changed (new components @@ -565,7 +646,7 @@ export class Registry { * @param namespace - Optional namespace for lookup priority * @returns Component configuration or undefined */ - getConfig(type: string, namespace?: string): ComponentConfig | undefined { + getConfig(type: string, namespace?: string): RegistryComponentConfig | undefined { // If namespace is explicitly provided, ONLY look in that namespace (no fallback) if (namespace) { const namespacedType = `${namespace}:${type}`; @@ -686,7 +767,7 @@ export class Registry { * * @returns Array of all component configurations */ - getAllConfigs(): ComponentConfig[] { + getAllConfigs(): RegistryComponentConfig[] { return Array.from(this.components.values()); } @@ -755,7 +836,7 @@ export class Registry { * @param namespace - Namespace to filter by * @returns Array of component configurations in the namespace */ - getNamespaceComponents(namespace: string): ComponentConfig[] { + getNamespaceComponents(namespace: string): RegistryComponentConfig[] { return Array.from(this.components.values()).filter( config => config.namespace === namespace ); diff --git a/packages/core/src/registry/__tests__/component-config-single-declaration.test.ts b/packages/core/src/registry/__tests__/component-config-single-declaration.test.ts new file mode 100644 index 0000000000..f840c6e885 --- /dev/null +++ b/packages/core/src/registry/__tests__/component-config-single-declaration.test.ts @@ -0,0 +1,278 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * Convergence pin — `ComponentConfig` has ONE declaration, and it is + * `@object-ui/types`' (objectui#6298). + * + * ## What was wrong + * + * Two PUBLISHED declarations of one name. `@object-ui/types`' `src/index.ts` + * exports the one in `base.ts`; `Registry.ts` reaches `@object-ui/core`'s + * public entry through `src/index.ts`'s `export * from './registry/Registry.js'`, + * with `package.json` mapping `"."` to `./dist/index.d.ts`. An IDE auto-import + * picked between them by alphabetical order — the failure mode the 2026-08-25 + * family ruling (decision 甲/A1 on objectui#6172) is about, and the disposition + * objectui#4580 ruled for this family: *a structural copy would reproduce the + * defect the moment either side moved.* + * + * objectui#6067 / PR #6297 closed HALF the delta by single-sourcing + * `ComponentMeta`. What survived it, and what this card closes, is GENERICITY + * AND THE `component` SLOT: `@object-ui/types`' was non-generic with + * `component: any`; core's was `` with `component: ComponentRenderer`. + * + * ## Why (b) — the re-export — and not (a), the derived declaration + * + * `scripts/__tests__/one-authority-per-exported-name-6273.test.ts` counts an + * `export type X = …` / `export interface X` as an AUTHORITY and deliberately + * does not count `export type { X } from './x'`. So a derived declaration in + * this package would have left the collision measured and the baseline row + * owed. That is not a prediction: `ComponentMeta` was converged the derived way + * by PR #6297 and is STILL a `KNOWN_COLLISIONS` row on this tree, three files + * away from the row this card removed. + * + * The stated proviso for (b) — *"needs `ComponentRenderer` to have a home + * `@object-ui/types` can reach"* — turns out not to bind, and the reason is + * pinned below: `ComponentRenderer` is the IDENTITY, so `component: T` in + * `@object-ui/types` is the same slot with nothing to import. It could not have + * been imported: `@object-ui/types` depends on `@objectstack/spec` and `zod` + * only, and this package depends on IT, so the edge would have been a cycle. + * + * ## Why the load-bearing assertions are not assignability + * + * Measured on the EMITTED `.d.ts` of both packages on the tree immediately + * BEFORE this convergence — an out-of-package consumer importing the name from + * each package: + * + * Exact = true <- on the DIVERGED pair + * Exclude = the five registry-only keys + * Exclude = never + * TypesConfig['component'] / CoreConfig['component'] = any / any + * TypesConfig = TS2315 "Type 'ComponentConfig' is not generic" + * + * Mutual assignability read `true` while the two types genuinely differed — + * `component: any` absorbs everything and every other member is optional — so + * an assignability assertion is a GHOST here, exactly as + * `component-meta-derives-from-canonical.test.ts` records for the sibling type. + * The readings that MOVED are the genericity of `@object-ui/types`' declaration + * and the source identity of this file, so those are the pins; the assignability + * pair is kept below, labelled, as the control that shows the contrast. + * + * ## Which declaration the type-level assertions read + * + * The EMITTED one, under `type-check`: `packages/core/tsconfig.test.json` sets + * `"paths": {}`, so `@object-ui/types` resolves through the workspace + * dependency's built `dist/index.d.ts`, and turbo's `type-check` task + * `dependsOn: ["^build"]`. Under vitest the same specifier resolves to `src/` + * and every annotation below is erased — `expect(x).toBe(true)` on a literal + * `true` proves nothing on its own. `tsc` is the enforcement; the runtime + * assertions exist so a failure has a named test to report against, the + * arrangement objectui#3181 recorded for this package. Deliberately NOT + * asserted here: anything read out of `packages/core/dist` — the CI `test` job + * runs `pnpm test` with no build ahead of it, and "an assertion whose colour + * depends on whether someone ran a build is not a pin". + */ + +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { describe, it, expect } from 'vitest'; +import type { ComponentConfig as CanonicalComponentConfig } from '@object-ui/types'; +import type { + ComponentConfig, + ComponentRenderer, + RegistryComponentConfig, + RegistryComponentMetaExtras, +} from '../Registry.js'; +import { Registry } from '../Registry.js'; + +/** + * Mutual-subset equality, wrapped in tuples so neither side distributes over a + * union and `never` compares as the empty set rather than vanishing. + */ +type Exact = [A] extends [B] ? ([B] extends [A] ? true : false) : false; + +/** True only for `any` — `0 extends 1 & X` can hold for nothing else. */ +type IsAny = 0 extends 1 & X ? true : false; + +/** A renderer stand-in with no relation to anything else in this file. */ +interface ProbeRenderer { + readonly __probe: 'renderer'; +} + +describe('ComponentConfig — the identity that made the convergence possible', () => { + it('keeps `ComponentRenderer` the identity alias, so `component: T` is the same slot', () => { + // THE load-bearing premise of objectui#6298. `@object-ui/types` declares + // `component: T` and cannot import `ComponentRenderer` (that edge would be + // a cycle). The two spellings mean the same thing ONLY while this alias is + // the identity. Give it real content and the convergence silently changes + // meaning — this is the assertion that refuses to let that happen quietly. + const rendererIsIdentity: Exact, ProbeRenderer> = true; + // …and its default arm, which is what the defaulted `ComponentConfig` rides on. + const defaultArmIsAny: IsAny = true; + + expect([rendererIsIdentity, defaultArmIsAny]).toEqual([true, true]); + }); +}); + +describe('ComponentConfig — the name this package publishes IS the canonical one', () => { + it('re-exports `@object-ui/types`’ declaration rather than a look-alike', () => { + // A structural equality check would pass on a member-identical COPY (that + // is objectui#4580's whole point, and the source-identity pin at the bottom + // of this file is what catches it). This one is still worth stating: after + // the re-export the two published names denote one type, parameter and all. + const sameTypeParameterised: Exact< + ComponentConfig, + CanonicalComponentConfig + > = true; + const sameTypeDefaulted: Exact = true; + + expect([sameTypeParameterised, sameTypeDefaulted]).toEqual([true, true]); + }); + + it('lets the type parameter reach the `component` slot from EITHER package', () => { + // The reading that actually moved. Before this card, + // `ComponentConfig` off `@object-ui/types` was + // `TS2315: Type 'ComponentConfig' is not generic`. + const throughTypes: Exact< + CanonicalComponentConfig['component'], + ProbeRenderer + > = true; + const throughCore: Exact['component'], ProbeRenderer> = true; + + expect([throughTypes, throughCore]).toEqual([true, true]); + }); + + it('leaves the bare spelling meaning exactly what it meant before (no consumer changes)', () => { + // The counter-probe the convergence has to survive. "One authority" is + // otherwise satisfiable by narrowing the slot for everyone, and narrowing + // is what was NOT permitted here: both declarations are published and the + // parameter was added DEFAULTED precisely so every existing spelling keeps + // its meaning. `ComponentConfig` is `ComponentConfig`, whose + // `component` is `any` — the slot `@object-ui/types` always published. + const bareSlotIsStillAny: IsAny = true; + const canonicalBareSlotIsStillAny: IsAny = true; + + expect([bareSlotIsStillAny, canonicalBareSlotIsStillAny]).toEqual([true, true]); + }); +}); + +describe('ComponentConfig — the registry-only keys were rehomed, not dropped', () => { + it('adds exactly the five registry-only keys on the entry type, named', () => { + // The convergence had to move the extras somewhere: the bare canonical + // declaration does not carry them, and `Registry.getNamespaceComponents` + // filters on `config.namespace`. They live on the named extension, and + // this is the symmetric difference that says so — spelled out as a literal + // union rather than compared against the extras type, which would be true + // by construction. + const registryOnlyKeys: Exact< + Exclude, + 'tier' | 'namespace' | 'skipFallback' | 'labelling' | 'deprecated' + > = true; + const extrasSupplyThem: Exact< + Exclude, + keyof RegistryComponentMetaExtras + > = true; + // Nothing on the canonical declaration went missing from the entry type. + const nothingLost: Exact, never> = + true; + + expect([registryOnlyKeys, extrasSupplyThem, nothingLost]).toEqual([true, true, true]); + }); + + it('hands the registry-only keys back off a real registration, at runtime', () => { + // The half no type-level assertion can reach on a vitest run: the entry the + // registry actually stores still carries the extras. A local instance, not + // the process-level `ComponentRegistry` singleton, so this file cannot + // perturb another test file. + const registry = new Registry(); + const renderer: ProbeRenderer = { __probe: 'renderer' }; + + registry.register('probe', renderer, { + namespace: 'issue-6298', + tier: 'internal', + labelling: 'group', + skipFallback: true, + label: 'Probe', + }); + + const config = registry.getConfig('probe', 'issue-6298'); + + expect(config?.type).toBe('issue-6298:probe'); + expect(config?.component).toBe(renderer); + expect([config?.namespace, config?.tier, config?.labelling, config?.skipFallback]).toEqual([ + 'issue-6298', + 'internal', + 'group', + true, + ]); + // …and the canonical half is readable off the same entry. + expect(config?.label).toBe('Probe'); + // The `namespace` filter is the live consumer that made rehoming mandatory. + expect(registry.getNamespaceComponents('issue-6298').map((c) => c.type)).toEqual([ + 'issue-6298:probe', + ]); + }); +}); + +describe('ComponentConfig — the assignability control (green on the diverged pair, kept to show the contrast)', () => { + it('is mutually assignable with the canonical declaration — and WAS before the convergence too', () => { + // Measured `true` on the emitted `.d.ts` of both packages while the two + // types genuinely differed. Recorded here as the control, not as the + // guarantee: if this pair were the only assertion in the file, reverting + // objectui#6298 would leave it green. + const bothWays: [ + ComponentConfig extends CanonicalComponentConfig ? true : false, + CanonicalComponentConfig extends ComponentConfig ? true : false, + ] = [true, true]; + + expect(bothWays).toEqual([true, true]); + }); +}); + +const REGISTRY_SRC = readFileSync( + fileURLToPath(new URL('../Registry.ts', import.meta.url)), + 'utf8', +); + +/** The re-export that makes this package share `@object-ui/types`' declaration. */ +const RE_EXPORT = "export type { ComponentConfig } from '@object-ui/types';"; + +/** The named extension that keeps the registry-only keys on the entry type. */ +const NAMED_EXTENSION = 'export type RegistryComponentConfig = ComponentConfig &'; + +/** A local declaration of the contested name, in either spelling. */ +const LOCAL_DECLARATION = /^\s*export\s+(?:declare\s+)?(?:interface|type)\s+ComponentConfig\b/m; + +/** The same shape for a name this file legitimately DOES declare — the control. */ +const LOCAL_EXTENSION_DECLARATION = + /^\s*export\s+(?:declare\s+)?(?:interface|type)\s+RegistryComponentConfig\b/m; + +describe('ComponentConfig — the source-identity pin', () => { + it('re-exports the canonical declaration and names the extension', () => { + expect(REGISTRY_SRC).toContain(RE_EXPORT); + expect(REGISTRY_SRC).toContain(NAMED_EXTENSION); + }); + + it('declares no `ComponentConfig` of its own', () => { + // This is THE pin. Every type-level assertion above stays green on a + // member-identical structural COPY — that is objectui#4580's ruling and the + // state this file exists to make unreachable. It is also what keeps the + // `KNOWN_COLLISIONS` row deleted: that gate counts a declaration here as a + // second authority and does not count the re-export. + expect(LOCAL_DECLARATION.test(REGISTRY_SRC)).toBe(false); + }); + + it('still matches the declaration it is meant to match — the pattern control', () => { + // A regex that matched nothing anywhere would pass the assertion above on + // any tree, including a re-diverged one. `RegistryComponentConfig` is + // declared here on purpose and has the identical shape, so the pattern is + // demonstrably live. It also proves the anchoring: the assertion above did + // not merely fail to see `RegistryComponentConfig`. + expect(LOCAL_EXTENSION_DECLARATION.test(REGISTRY_SRC)).toBe(true); + }); +}); diff --git a/packages/types/src/base.ts b/packages/types/src/base.ts index bf5ca5162e..7c41e6b1ad 100644 --- a/packages/types/src/base.ts +++ b/packages/types/src/base.ts @@ -637,9 +637,39 @@ export interface ComponentMeta { } /** - * Complete component configuration combining renderer and metadata. + * Complete component configuration combining renderer and metadata — the ONE + * declaration of this name (objectui#6298). + * + * ## Why it carries a type parameter + * + * `@object-ui/core` used to declare a SECOND `ComponentConfig` in + * `src/registry/Registry.ts`, and after objectui#6067 / PR #6297 single-sourced + * the `ComponentMeta` half, GENERICITY AND THE `component` SLOT was the whole + * of what still made the two same-named PUBLISHED exports genuinely different + * types: core's was `` with a parameterised renderer slot, this one was + * non-generic with `component: any`. Core now RE-EXPORTS this declaration, so + * the parameter has to live here for that re-export to be lossless. + * + * The parameter is DEFAULTED, so every existing spelling keeps its meaning + * exactly: bare `ComponentConfig` is `ComponentConfig`, whose `component` + * is `any` — the same slot this declaration has always published. Nothing that + * compiled against it before has to change. + * + * ⚠️ `T` is the RENDERER ITSELF, not a props type. Core's `ComponentRenderer` + * is the IDENTITY alias (`export type ComponentRenderer = T`), which is + * why `component: T` here says exactly what `component: ComponentRenderer` + * said there — and why this convergence needed no `@object-ui/types` → + * `@object-ui/core` dependency edge, which would have been a cycle in the wrong + * direction (this package is the bottom layer; core depends on it). That + * identity is not assumed: it is pinned in + * `packages/core/src/registry/__tests__/component-config-single-declaration.test.ts`, + * and if `ComponentRenderer` ever stops being the identity this slot stops + * matching it and that pin goes red. + * + * @typeParam T - The renderer this configuration carries. Defaults to `any`, + * the framework-agnostic slot this package published before it was named. */ -export interface ComponentConfig extends ComponentMeta { +export interface ComponentConfig extends ComponentMeta { /** * Unique component type identifier */ @@ -648,7 +678,7 @@ export interface ComponentConfig extends ComponentMeta { /** * The component renderer (framework-specific) */ - component: any; + component: T; } /** diff --git a/scripts/__tests__/one-authority-per-exported-name-6273.test.ts b/scripts/__tests__/one-authority-per-exported-name-6273.test.ts index b0f1027154..c71aac9e30 100644 --- a/scripts/__tests__/one-authority-per-exported-name-6273.test.ts +++ b/scripts/__tests__/one-authority-per-exported-name-6273.test.ts @@ -365,7 +365,18 @@ const KNOWN_COLLISIONS: ReadonlyMap = new Map([ ['ChatMessage', ['packages/plugin-chatbot/src/ChatbotEnhanced.tsx', 'packages/types/src/complex.ts']], ['ChatToolInvocation', ['packages/plugin-chatbot/src/ChatbotEnhanced.tsx', 'packages/types/src/complex.ts']], ['ComboboxOption', ['packages/components/src/custom/combobox.tsx', 'packages/types/src/form.ts']], - ['ComponentConfig', ['packages/core/src/registry/Registry.ts', 'packages/types/src/base.ts']], + // `ComponentConfig` sat here, colliding between + // `packages/core/src/registry/Registry.ts` and `packages/types/src/base.ts`. + // objectui#6298 made `@object-ui/types` the one authority — it gained the + // defaulted type parameter that was the last real difference between the two + // — and `Registry.ts` now RE-EXPORTS it, which this gate does not count. The + // entry would fail the stale-baseline direction. + // + // ⚠️ `ComponentMeta` below is the same PAIR OF FILES and is deliberately + // still here: PR #6297 converged its SHAPE (`CanonicalComponentMeta & + // RegistryComponentMetaExtras`) but left a declaration in `Registry.ts`, and + // a derived declaration is still an authority. That contrast is why #6298 + // took the re-export route rather than the derive route. ['ComponentMeta', ['packages/core/src/registry/Registry.ts', 'packages/types/src/base.ts']], ['ConditionalFormattingRule', ['packages/plugin-kanban/src/KanbanEnhanced.tsx', 'packages/plugin-kanban/src/KanbanImpl.tsx', 'packages/types/src/objectql.ts']], ['ConfirmDialogState', ['packages/app-shell/src/views/ActionConfirmDialog.tsx', 'packages/plugin-designer/src/hooks/useConfirmDialog.ts']],