Skip to content

ComponentMeta: re-export the one declaration, then deprecate PluginComponentMeta at its final meaning - #6068

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-5893-componentmeta-convergence
Aug 24, 2026
Merged

ComponentMeta: re-export the one declaration, then deprecate PluginComponentMeta at its final meaning#6068
yinlianghui merged 1 commit into
mainfrom
claude/issue-5893-componentmeta-convergence

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#5893

Direction (b) as ruled: converge, then retire — in that order.

packages/types/src/plugin-scope.ts' ComponentMeta becomes export type { ComponentMeta } from './base.js', and the published alias ComponentMeta as PluginComponentMeta is then deprecated at its final meaning. This inherits #4580's ruling with its reason — "a structural copy would reproduce the defect the moment either side moved" — and follows #5671's executed shape for the sibling type ComponentInput, in the same file. No second convergence shape was invented: the diff is line-for-line the move #5671 made, one type over.

The STOP condition was looked for first, and is not present

The ruling's stop was: if the plugin surface deliberately excludes tags/description, stop and report. Searched before converging, in the four places named:

whereresult
plugin-scope.ts' module header (@module plugin-scope, "Section 3.3: Plugin scope isolation types")scope isolation only — nothing about metadata field selection
the declaration's own docblockone line: Component metadata for registration. No exclusion, no reason
#4580's thread (all 5 comments)rules on SchemaNode, label vocabulary, visible/disabled. ComponentMeta not mentioned
repo-wide ADR/doc grep for deliberate-omission languagehits are unrelated (page-app-dashboard-spec-parity.test.ts' spec-key omissions, an objectql.tsviewMode note)

And the positive evidence runs the other way: the zod mirror never diverged.ComponentMetaSchema (packages/types/src/zod/base.zod.ts:298) declares all eleven keys including tags and description. Two of the three authorities carried them and the plugin-facing declaration did not — drift, not design. Converging.

The delta, re-derived on this merge-base (efa70ecb2), key by key

packages/types moved five times on 2026-08-23/24, so this is measured here rather than carried from the card.

keybase.ts:511plugin-scope.ts:149ComponentMetaSchema
labeliconcategoryinputsdefaultPropsexamplesisContainerresizable✅ ×8✅ ×8✅ ×8
resizeConstraints✅ (6 members)✅ (same 6)✅ (same 6)
tagsstring[]z.array(z.string())
descriptionstringz.string()
totals11911

The delta is unchanged since the card was filed: exactly two keys, same two, neither grown nor closed.resizeConstraints' six members verified identical, so the two-key delta is the whole of it.

Consumers of PluginComponentMeta, re-measured with controls

Same method for probe and controls — grep -rn, excluding node_modules/, dist/, .git/, over every root present in the tree (packages/ apps/ content/ docs/ skills/ examples/ e2e/ scripts/ eslint-rules/ public/ .changeset/ and root docs), plus the sibling objectstack framework checkout.

namehitsreading
PluginComponentMeta (probe)1its own export line, packages/types/src/index.ts:942. Nothing imports it
AppMetadataPlugin (control — neighbour in the same export type { … } block)13includes a real cross-package import, packages/core/src/registry/PluginSystem.ts:10
PluginComponentInput (control — the sibling alias #5674 deprecated)13found by the same apparatus
PluginComponentMeta in objectstack checkout0with controls there: @object-ui/types 23 files, SchemaRenderer 36 files

A zero with an apparatus that demonstrably finds names that are there.

⚠️ In-repo zero is not npm zero, and that caveat is not being dropped. It is restated in the changeset in full. This is why the alias is deprecated, not deleted#5674's two-stage pattern (maintainer ruling, 2026-08-22: deprecate for a release, then remove). Stage 2 removes the alias and the now-dead ComponentInput/ComponentMeta re-exports in plugin-scope.ts that exist only to feed the aliases.

Why the ordering is load-bearing. Until this PR the alias named a genuinely different nine-key interface. Deprecating it first would have told consumers to stop using a name that was still about to change meaning. It is tagged now, at its final meaning — and a test asserts those two facts together so the sequence cannot be undone by halves.

Verification: the divergence is pinned by IDENTITY, not by member set

New file: packages/types/src/__tests__/component-meta-single-declaration.test.ts (13 tests).

The load-bearing assertion is that plugin-scope.tsdeclares no ComponentMeta of its own and re-exports base.ts'. A member-set assertion cannot do this job, and the file says so rather than assuming it: TypeScript is structurally typed, so a local re-declaration carrying the same eleven keys is mutually assignable with the imported one. A member-identical structural copy is exactly what #4580 predicted would drift, and exactly what this card is the proof of — this copy started identical and acquired its two-key delta later.

Ablation — direction predicted before running, and one prediction was wrong

Prediction, written to PREDICTION.txt before any mutation: replacing the re-export with a member-identical local export interface ComponentMeta (all eleven keys) turns the identity pin red while every member-set / type-level assertion stays green, and type-check stays green.

Leg 1 — vitest. Matched exactly: 3 failed | 10 passed.

red (3)green (10, selected)
identity pin › re-exports the declaration from base.tsmember-set control › is mutually assignable across both spellings
identity pin › declares no ComponentMeta of its ownmember-set control › reaches the published entry point under both names
deprecation › is deprecated only AFTER the convergencecounter-probe › lets a plugin registration write tags and description
identity pin › base.ts is the declaration site (regex non-vacuity control — stayed green, so the pin is not matching-nothing)

Leg 1's type-check read was CONFOUNDED and its prediction was wrong. I predicted exit 0; it exited 2, with src/plugin-scope.ts(19,15): error TS2440: Import declaration conflicts with local declaration of 'ComponentMeta'. That red is an artifact of my mutation leaving the file's top-of-file import type { ComponentMeta } in place beside the injected local declaration — a name collision, saying nothing about whether the member-set assertions can see a structural copy. Reported rather than quietly re-rolled.

Leg 2 — the controlled repeat. Same member-identical interface, and the colliding import removed as well (which is precisely the pre-#5893 file shape, plus the two keys). Read: pnpm --filter @object-ui/types type-checkexit 0, green, its own verdict line clean across all three tsc invocations.

That is the decisive measurement. Every type-level assertion in the suite — mutual assignability both ways, the published-entry-point check, and the tags/description counter-probe — compiles clean against a re-declared copy. Only the source-level identity pin sees it. Which is the contrast the pin exists for.

Ablation hygiene, both legs: anchor uniqueness asserted before writing (count == 1, asserted inside the mutation script so a missed anchor aborts rather than silently no-ops); landing site printed (plugin-scope.ts:177 / :176); the mutation proved on disk by grepping the injected text and separately the removed text, never by an editor exit code; restore under trap … EXIT INT TERM using absolute paths only, no cd, so it is cwd-independent; and restore verified afterwards rather than assumed — the restored file is sha256-identical to the pre-mutation backup, and git status --porcelain shows only the five intended files.

No package boundary is crossed by this ablation and no dist/ is involved: the pin reads ../plugin-scope.tssource, and the type imports resolve to src/ under vitest. Stated because it is the reason no rebuild-between-mutation-and-read step appears here — not an omission of one.

Counter-probe: consumers still type-check

"The duplicate is gone" must not be satisfiable by breaking the type for everyone.

  • In-suite: a realistic plugin registration writing labeliconcategoryinputsisContainerresizabletagsdescription against the plugin-scoped spelling compiles and its values round-trip. Those two keys were a plain TS error on this spelling before this PR.
  • At scale: the downstream sweep below, 43 packages, all green.

Gates, each read from its own verdict line, all on the final commit 3640aba1a

git status --porcelain was empty when these ran, so they read the committed tree.

gateexitverdict
pnpm --filter @object-ui/types type-check (script name echoed: type-check, hyphenated)0tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json — all three. The third is why the type-level assertions and @ts-expect-errors are real enforcement
pnpm exec vitest run packages/types/src --maxWorkers=2 (root form)0Test Files 56 passed (56) · Tests 619 passed (619)
the new suite alone, --reporter=verbose013 passed, each named — confirming it actually ran rather than zero-matching
pnpm --workspace-concurrency=4 --filter '...@object-ui/types' build042 distinct packages executed; zero error TS, zero ERR_PNPM
pnpm exec eslint . in packages/types (plain, no --no-inline-config)0242 problems (0 errors, 242 warnings) — all pre-existing no-explicit-any
check:control-bytes0✅ OK (scanned 5013 tracked text file(s); skipped 85 binary)
check-changeset-presence0✅ 4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)
check-changeset-fixed0✅ All workspace packages are in the changeset fixed group
check-changeset-no-major0✅ No changeset declares a major bump
check:esm-specifiers0no un-ledgered package emits an extensionless relative specifier
check:self-import0✅ No package names itself inside its own src/ (44 packages, 16798 specifiers)
check:phantom-deps0✅ Every in-scope import is declared by the package that publishes it
check:doc-types0✅ Every documented component type is registered
check:published-dist0✅ No published package's build output carries tooling material

Exit codes captured before any pipe (cmd > log 2>&1; EXIT=$?), never from a $? following a tail.

Notes on two gates.

The downstream sweep direction is downstream--filter '...@object-ui/types' is the prefix form, i.e. the 43 packages that depend on@object-ui/types. That is the correct direction for a contract change in this package: the widened type flows to consumers. @object-ui/react-runtime was built separately first, per the gotcha measured on #5927 — it does not depend on @object-ui/types, so it is never in the prefix closure, and without it apps/site and packages/components fail for reasons unrelated to any change. Not a finding; just the prerequisite.

Nothing is claimed about rejection. Per #6058: check:zod-mirror-parityis not a gate in this repository — grepped .github/workflows/, scripts/, and root package.json, zero hits — so there is no ratchet here to over-credit for a TS-side-only change. And independently of that: ComponentMetaSchema is a plain z.object with no .strict(), so it strips unknown keys rather than refusing them (measured on zod 4.4.3 by default-children-retired-contract-twins.test.ts). This convergence buys ACCEPTANCE of two keys on the plugin-facing type. It buys no rejection of anything, anywhere. The BaseSchema index-signature ceiling (#5155) is untouched — ComponentMeta does not extend BaseSchema, so it was never on that axis.

Blast radius

Nothing narrows: no key removed, no key's type changed. Two optional keys become writable on one spelling. That is why 43 downstream packages build green and why the changeset is a minor, not a major (this repo's own breaking changes never declare major).

Also in this diff

default-children-retired-contract-twins.test.ts — the file #5893 named as "the running cost of the copy" — has its header corrected. Its case 3 no longer exercises a second declaration; it exercises a second published spelling of the first. It is deliberately kept, not deleted: while the deprecated alias is published, an author can still reach the type by that name, and the pin costs one @ts-expect-error. It is marked to be dropped together with the alias in stage 2.

Out of scope, filed not fixed

#6067@object-ui/core's Registry.ts:37 is a thirdComponentMeta, and it is missing tags/description too: the same two keys, now the only remaining structural copy of the name in the workspace. It is a superset-with-holes rather than a copy (it adds tier, namespace, skipFallback, labelling), so it cannot be a bare re-export and needs a shape decision — which is why it is filed for triage rather than folded in here. #5671 converged ComponentInput in that very file and left this one, deliberately, as out of its fence. Searched open issues before filing; #4972 is closed and never names ComponentMeta, and #4631 is the wider three-surfaces card, not this.

packages/core was not touched. objectql.ts was not touched (#5903's PR #6053 is in flight there). No file under app-shell/src/utils/, plugin-kanban, plugin-detail, data-objectstack, apps/console, or metadata-admin/previews/ was touched.

Generated by Claude Code


Generated by Claude Code

…ComponentMeta
`plugin-scope.ts`' `ComponentMeta` becomes `export type { ComponentMeta } from
'./base.js'` — objectui#4580's ruling applied to the second member of the family,
following the shape objectui#5671 executed for the sibling type `ComponentInput`
in the same file.
Either side had moved: `base.ts` carried eleven keys, the plugin-scoped copy
nine — the same nine minus `tags` and `description`. `ComponentMetaSchema` (the
zod mirror) already declared all eleven, so two of three authorities agreed and
the plugin-facing one did not.
The published alias `PluginComponentMeta` is then deprecated at its final
meaning, on objectui#5674's two-stage pattern. The ordering is the point:
deprecating first would have warned about a name still about to change meaning.
Pinned by IDENTITY, not by member set — a member-identical structural copy
passes every type-level check (measured: `type-check` exits 0 on one), which is
exactly the state objectui#4580 predicted would drift.
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3220.9 KB3990.2 KB
Main entry chunk (gzip)153.7 KB350 KB
Entry fileindex-QwStSeLb.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)10.38KB3.90KB
app-shell (runtime-config.js)18.10KB6.51KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)505.15KB114.53KB
core (index.js)4.92KB1.97KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)166.86KB46.08KB
fields (index.js)238.40KB59.89KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.21KB44.67KB
plugin-dashboard (index.js)133.35KB34.44KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)244.12KB61.87KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)125.63KB30.64KB
plugin-gantt (index.js)164.15KB39.88KB
plugin-grid (index.js)200.79KB54.26KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.86KB27.22KB
plugin-map (index.js)20.11KB6.64KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.49KB7.59KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.57KB20.74KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)52.40KB17.45KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.35KB0.70KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.49KB2.14KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@yinlianghui
yinlianghui marked this pull request as ready for review August 24, 2026 15:42
@yinlianghui
yinlianghui added this pull request to the merge queueAug 24, 2026
Merged via the queue into main with commit 6d1c155Aug 24, 2026
23 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-5893-componentmeta-convergence branch August 24, 2026 15:54
yinlianghui pushed a commit that referenced this pull request Aug 24, 2026
Remove `DashboardConfig.aria` (packages/types/src/designer.ts) and turn its Zod
twin `DashboardConfigSchema.aria` (packages/types/src/zod/complex.zod.ts) into an
ADR-0049 retirement tombstone.
The pair declared `{ label?, description? }` — spellings that match neither
@objectstack/spec's `AriaProps` (`ariaLabel`/`ariaDescribedBy`/`role`) nor any
renderer vocabulary, so no read point could have consumed them. Re-measured on
main: zero `.aria` reads in plugin-designer/src, plugin-dashboard/src and
apps/console/src (the same grep family finds the live `schema.aria` reads in
plugin-detail and plugin-list); zero occurrences of either name in objectstack;
and DashboardConfigPanel.tsx — the panel the interface's doc comment claims to
serve — imports neither name.
- TS half: plain deletion + in-place comment tombstone, mirroring the landed
shape of #5855 (#5830) on the sibling `DashboardComponentSchema.aria`. The
interface's `[key: string]: any` catch-all means an authored `aria` still
compiles; what goes is the type-level suggestion and the false contract claim.
- Zod half: `z.never({ error }).optional().describe(...)` rather than a plain
delete. Measured: `DashboardConfigSchema` is a bare `z.object` with no
`.strict()`, so a deletion would have turned an accepted-and-PRESERVED key
into a silently STRIPPED one (the #6068 behaviour). Loud refusal is this
package's ruled outcome for retirements (data-display.zod.ts
StaticTableColumnSchema, the set crud.zod.ts `confirm` established).
- Tests: the aria-only pin in dashboard-config.test.ts is FLIPPED, not deleted —
it now asserts refusal by name and message, with two controls (an undeclared
key still rides through, proving the red is the tombstone and not strictness;
a legal config still parses). The `aria:` line in the full-config TS literal is
replaced by a removal note; kept, it would have green-washed through the index
signature.
Claude-Session: https://claude.ai/code/session_01CSoz9uGhaaSgiq3hshtN7L
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@yinlianghui@claude