Uh oh!
There was an error while loading. Please reload this page.
ComponentMeta at the registry: derive it from the one declaration, extend it under a named type - #6297
Conversation
…aration `packages/core/src/registry/Registry.ts` carried its own thirteen-key `ComponentMeta`: nine members restated from `@object-ui/types`' `base.ts`, four registry-only keys added here, and `tags` / `description` absent — though 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. Same two-key delta objectui#5893 closed inside `@object-ui/types`, on a third declaration. The nine shared members are no longer restated. `ComponentMeta` is now `CanonicalComponentMeta & RegistryComponentMetaExtras`, where the newly exported `RegistryComponentMetaExtras` carries `tier` / `namespace` / `skipFallback` / `labelling` — the four keys that are genuinely registry mechanics and have live consumers. Nothing is removed or renamed; `tags` and `description` are added. Pinned by key set rather than by assignability. Every member of both shapes is optional, so `extends` is mutually true across the diverged pair and an assignability assertion is green on the defect. Measured on the emitted `.d.ts` of both packages, `Exclude<keyof Canonical, keyof Core>` moved from `"tags" | "description"` to `never` while both `extends` readings stayed `true`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mn4BZ5AVDM81pvfij1WwM9
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
os-litant
commented
Aug 25, 2026
The out-of-scope finding named at the end of the description is filed as #6298 ( Also: the downstream consumer type-check row in the description was re-run at the final commit Generated by Claude Code |
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#6067
@object-ui/core'sComponentMetano longer restates the canonical declaration. It is now derived from it, andtags/descriptionreach the registration surface.1. Premise, re-derived on today's
main— it holdsRe-derived from scratch at merge-base
c456d91f4, not carried from the card.Declaration census. A repo-wide search for a
type/interface/classdeclaration of the exact name returns exactly two:packages/types/src/base.ts:511export interface ComponentMeta {— the canonical onepackages/core/src/registry/Registry.ts:37export type ComponentMeta = {— the structural copySo core's is the only remaining structural copy, exactly as the 04:18Z triage comment graded it, and the count the first triage comment (16:29Z) asked to be re-verified on the merged ref is confirmed: #5893 collapsed
packages/types' two onto one, and the card's title ("a THIRD declaration") is the pre-#5893 count.Control probe for the census, because the whole task is counting declarations and a zero-hit is not a reading on its own: the same pattern run against the sibling type
ComponentInputreturns exactly one declaration (packages/types/src/base.ts:379) — the state #5671 left it in — and the pattern matches both theexport typeand theexport interfacespellings, which is where the card's own filing probe went wrong.Key sets, re-counted — canonical 11, core 13, symmetric difference 6, all confirmed:
tags,descriptiontier,namespace,skipFallback,labellinglabel,icon,category,inputs,defaultProps,examples,isContainer,resizable,resizeConstraints— includingresizeConstraints, whose six members were identical2. The disposition: converge, not rename
The ruling admitted two resolutions. This PR converges, and the reason is that rename cannot be executed here without a contract break:
@object-ui/coreis published, andComponentMetais part of its export surface:src/index.tscarriesexport * from './registry/Registry.js', andpackage.jsonmaps"."→./dist/index.d.ts.The other rejected option was moving the four registry keys onto
@object-ui/types'ComponentMetaand re-exporting outright, the way #5671 handledComponentInput.skipFallbackandnamespacedescribe how the registry keys an entry, not what a component is, and@object-ui/types'ComponentMetais the general, plugin-facing, AI-facing type. They stay where they are read, under their own named type.Nothing is removed and nothing is renamed. All fifteen keys on the resulting type are writable; two of them are new.
3. The four registry-only keys ARE published behaviour with consumers
Requested by the dispatch as the measurement that decides whether converge-with-extension is safe. It is, and this is why a rename would not have been.
namespacepackages/**Registry.ts:247,Registry.ts:315— the bare-key fallback decisionskipFallbackaction-bar.tsx,action-button.tsx,record-approvals-renderer.tsx, …)Registry.ts:247,Registry.ts:315, same decisionlabellingFIELD_WIDGET_LABELLING(packages/fields/src/index.tsx:2934); 14 of them (10group+ 4display) write the key into the meta atindex.tsx:3009—'control'is spelled as absencepackages/components/src/renderers/form/form.tsx:446—ComponentRegistry.getMeta(…)?.labellingpackages/fields/src/index.tsx:2936,packages/app-shell/…/DashboardWidgetInspector.tsx:581andpackages/app-shell/…/widgets.tsx:2547all take an indexed accessComponentMeta['labelling']off the type published by@object-ui/coretierPUBLIC_BLOCKSlist;tier: 'public'is the documented per-registration opt-in (ADR-0080)Registry.ts:560,Registry.ts:564(getPublicConfigs),packages/sdui-parser/src/index.ts:148(publicOnly)labellingadditionally appears in three shipped CHANGELOGs as "@object-ui/core—ComponentMetagainslabelling?: 'control' \| 'group'", so it is published behaviour on the record, not merely in-repo usage.Direct importers of the type itself:
packages/fields/src/index.tsx:11,packages/app-shell/…/widgets.tsx:44,packages/app-shell/…/DashboardWidgetInspector.tsx:33.4. The measurement that shaped the pin:
extendsis a ghost hereEvery member of both shapes is optional, so a type with fewer optional members is assignable to one with more and vice versa. Mutual assignability therefore holds across the diverged pair — an
extends-based mirror assertion is green on the defect and could never have caught it.Measured with a TypeScript program resolved against the emitted
.d.tsof both packages (packages/types/dist/index.d.tsandpackages/core/dist/registry/Registry.d.ts), before and after:Core extends CanonicaltruetrueCanonical extends CoretruetrueExclude<keyof Canonical, keyof Core>"tags" | "description"neverExclude<keyof Core, keyof Canonical>"tier" | "namespace" | "skipFallback" | "labelling"Exactly one reading moved. That reading is the pin; the assignability pair is kept beside it in the test, labelled, as the control that demonstrates the contrast.
This also means the triage wording "asserting a mirror relationship that is false in both directions" is true of the key sets and false of TypeScript assignability. Recorded because acting on the assignability reading would have produced a test that cannot fail.
5. Pinning against the emitted declaration, not the source
The dispatch flagged the #6267 trap: a source-level assertion passing while the shipped
.d.tsis wrong. Measured rather than assumed for this package:@object-ui/core'sbuildis a baretsc, not a.d.tsbundler, so a module re-export survives into the emit rather than being inlined. Natural control:ComponentInput: re-export the one declaration instead of restating it three times #5671'sexport type { ComponentInput } from '@object-ui/types';appears verbatim atdist/registry/Registry.d.ts:31on the pre-change tree. After this PR,dist/registry/Registry.d.tslikewise carriesimport type { ComponentMeta as CanonicalComponentMeta } from '@object-ui/types';andexport type ComponentMeta = CanonicalComponentMeta & RegistryComponentMetaExtras;— so the shipped declaration references the canonical one by module specifier. There is one declaration of the shared eleven in the published artifact, not a copy of it.packages/core/tsconfig.test.json, which sets"paths": {}so@object-ui/typesresolves through the workspace dependency's builtdist/index.d.ts. That project is chained from this package'stype-checkscript, and turbo'stype-checktaskdependsOn: ["^build"], so CI's Type Check job builds the dependency before compiling it. The canonical half of every assertion is therefore read off the emitted declaration.packages/core/dist. CI'stestjob runspnpm test(rootvitest run) with no build ahead of it at all, so a dist-reading assertion would be absent-or-red depending on whether someone ran a build — the standard this repo already records insrc/actions/__tests__/actionKeys.types.test.ts: an assertion whose colour depends on whether someone ran a build is not a pin. Thepackages/core/disthalf is covered by the out-of-band measurement above and reported here rather than encoded as a green-by-luck test.6. Evidence — per-point ablation
Direction predicted before each run; both predictions held. Each ablation restored under
trap … EXIT INT TERMwith a cwd-independentgit -C … checkout HEAD --, and each restore printed an emptygit diff HEAD --stat. Mutations proved on disk by grepping the injected and the removed text separately, with anchor uniqueness asserted before the edit. Committed before ablating.A. Revert only the convergence (
Registry.tsback toorigin/main; test and changeset kept)Predicted: the key-set pin and the
tags/descriptioncounter-probe go red; the assignability control stays green.tsc -p tsconfig.test.json→ exit 2, six diagnostics, all naming the pin file:Root
vitest→ exit 1,Tests 2 failed | 5 passed (7):imports the canonical declaration instead of restating it,declares none of the canonical members locally.Still passing after the revert — reported because it is the point of the exercise:
is mutually assignable with the canonical declaration— no diagnostic at all undertsc, and green under vitest. This is the ghost, demonstrated on the real defect.adds exactly the four registry-only keys, named(line 113) — no diagnostic: the pre-convergence type also carried exactly those four, so this assertion is correctly indifferent to the convergence.still declares the four registry-only members here— green: the old declaration still declared them.tags/descriptioncounter-probe pass under vitest, because annotations are erased there.tscis their enforcement, which is why both legs are reported.B. Delete exactly one registry-only key (
labelling) fromRegistryComponentMetaExtrasPredicted: the named-keys assertion goes red while the canonical-key pin stays green — proving the two are independent instruments, not one assertion written twice.
Anchor asserted unique (1 occurrence) before the edit; landing site
1 file changed, 1 deletion(-).tsc -p tsconfig.test.json→ exit 2, three diagnostics:Root
vitest→ exit 1,Tests 1 failed | 6 passed (7):still declares the four registry-only members here.Still passing:
leaves no canonical key unreachable at the registration surface(line 101 — no diagnostic), the assignability control, and — informatively —extrasSupplyThem(line 122), which compares the extras type against itself and so shrinks with it. That contrast is exactly why the four keys are pinned to a literal union as well.7. Gates run locally
All exit codes captured before any pipe. Union run at
8ef81a944, the final commit.turbo run build --filter=@object-ui/core --force2 successful, 2 totalpnpm --filter @object-ui/core type-check(tsc --noEmit && tsc -p tsconfig.test.json)pnpm exec vitest run packages/core/(repo root)Test Files 100 passed (100),Tests 2023 passed (2023)pnpm exec vitest run packages/core/src/registry/__tests__/component-meta-derives-from-canonical.test.tsTests 7 passed (7)turbo run type-check --filter='...@object-ui/core' --filter='!@object-ui/site' --forceTasks: 71 successful, 71 totalnode scripts/check-changeset-presence.mjsnode scripts/check-changeset-no-major.mjsmajorbump."pnpm --filter @object-ui/core lintno-explicit-anywarnings; the new test file contributes none)node scripts/check-control-bytes.mjsnode scripts/check-type-check-coverage.mjsnode scripts/check-lint-coverage.mjsnode scripts/check-phantom-dependencies.mjsnode scripts/check-package-self-import.mjsnode scripts/check-vi-mock-specifiers.mjsnode scripts/check-doc-component-types.mjsnode scripts/check-shell-escape-residue.mjsFilter direction demonstrated, not asserted — the consumer direction is the prefix form, and it is the one used above:
Declared narrowings.
pnpm test/pnpm lint/pnpm type-checkrepo-wide,check:readme-exports,check:doc-snippets,check:esm-specifiers,check:node-esm-load,check:published-distandcheck:eager-closurewere not run locally; CI runs the farm regardless. The downstreamtype-checkrow above is the load-bearing one for a published-type change and it was run in full.@object-ui/siteis excluded from the downstream row for runtime; it consumes@object-ui/coreonly through documentation snippets, whichcheck:doc-snippetscovers in CI.8. Scope
packages/core/src/registry/Registry.ts,packages/core/src/registry/__tests__/component-meta-derives-from-canonical.test.ts,.changeset/6067-component-meta-derive-from-canonical.md. Nothing outsidepackages/core/src/registry/**plus the mandatory changeset.packages/typeswas not edited. Convergence did not require it: the canonical declaration already carries everything core needed, so importing it was sufficient. That package is held by fix(types): StackSchema ships its declared members instead of collapsing under BaseSchema's index signature #6267.content/docs/guide/component-registry.mdis the only doc that namesComponentMeta; it shows a registration literal and does not enumerate the key set, so nothing it states became false. Enumerating the newly reachable keys there would be a docs change outside this card's file surface.Out-of-scope finding, not fixed here
ComponentConfigis the same species one type down:packages/core/src/registry/Registry.ts:141declaresexport type ComponentConfig<T = any> = ComponentMeta & { … }whilepackages/types/src/base.ts:578declaresexport interface ComponentConfig extends ComponentMeta { … }— two same-named published declarations, differing in genericity and in whichComponentMetahalf they build on. This PR reduces it (the shared eleven are now single-sourced throughComponentMetaon both) without closing it. Filed separately, unassigned, as afinding. #4631 does not cover it — that card is about a component type's three disagreeing surfaces (schema type / registryinputs/ renderer reads), and namesComponentConfignowhere.Generated by Claude Code