Skip to content

feat(types): close the dashboard widget type vocabulary and gate the plugin-dashboard catalog - #6003

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-4600-schema-catalog-dashboard-gate
Aug 24, 2026
Merged

feat(types): close the dashboard widget type vocabulary and gate the plugin-dashboard catalog#6003
yinlianghui merged 2 commits into
mainfrom
claude/issue-4600-schema-catalog-dashboard-gate

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#4600

Implements the maintainer ruling of 2026-08-14 (objectstack#8593) — the (d2) remainder of this card. (d1) landed earlier as #4615 and is not redone here; re-verified rather than assumed (see below).

The card's premise, re-measured — and falsified

The card's headline was "9 of 9 REJECT", measured at 92250d648 against @objectstack/spec's DashboardSchema. The ruling says that is the wrong schema for these documents: an SDUI dashboard component node validates against objectui's own component schema, and the spec's DashboardSchema governs stored metadata documents only.

Re-measured on this tree with the schema the ruling names:

ACCEPT basic-dashboard.json ACCEPT filtered-dashboard-filter-types.json
ACCEPT e-commerce-dashboard.json ACCEPT filtered-dashboard-target-widgets.json
ACCEPT filtered-dashboard-dataset-widgets.json ACCEPT filtered-dashboard.json
ACCEPT filtered-dashboard-date-presets.json ACCEPT support-dashboard.json
ACCEPT filtered-dashboard-dynamic-options.json
=== 0 of 9 REJECT under objectui DashboardComponentSchema

9 of 9 accepted. Classes A and B were artefacts of the wrong schema, exactly as the ruling says, so no identity keys were added and no envelope was stripped. C1 re-verified as already landed: the six filtered-* entries carry the live dataset / dimensions / values and options shapes, not the retired inline-analytics form.

But acceptance alone would have been a gate that validates nothing

Measured on this tree before any change, the same DashboardComponentSchema.safeParse also accepted:

inputverdict (before)
{ type: 'zzz-not-a-widget-type', title: 'x' }ACCEPT
a widget with no type at allACCEPT
{ type: 'metric', categoryField: 'stage', aggregate: 'sum' }ACCEPT — the three keys silently stripped

DashboardWidgetSchema.type was string on the interface and z.string() in the Zod twin. So a gate asserting .success would have passed by validating almost nothing — the outcome this card explicitly names as worse than no gate. Closing that hatch is what makes the gate real, and it is also literally what the ruling asks for: metric-card joins a CLOSED enum, not an open "extension allowed" hatch.

What this PR does

1. Closes the widget type vocabulary (packages/types), composed three ways, each by its own provenance:

  • the spec's 20 families by reference off ChartTypeSchema.options — restating them would reproduce the "narrower than the contract it implements" bug this file already records for label and defaultRange;
  • DASHBOARD_WIDGET_TYPE_EXTENSIONS = list, custom — the pre-existing objectui-only families, until now carried only as the prose "widened to z.string()" and enforced nowhere;
  • DASHBOARD_COMPONENT_WIDGET_TYPES = metric-card — objectui's own CLOSED component enum, per the ruling, explicitly not the spec widget enum. @objectstack/spec is untouched; Clause-② stays no.

TS (DashboardWidgetTypeName) and Zod (DashboardWidgetTypeSchema) both close, so declared = enforced. The 2026-08-17 TypeScript-face comment was verified, not re-derived: DashboardComponentSchema is at complex.ts (now :800), there is still no per-family widget type, and MetricCardProps is still not re-exported — nothing here re-exports it or invents a MetricCardSchema.

2. Adds the standing gateexamples/schema-catalog/test/plugin-dashboard-component-schema.test.ts. Three checks per entry: it parses under DashboardComponentSchema; every widget names a type in the closed vocabulary; no authored key is silently dropped. The third routes each widget to the schema that actually owns it — a metric-card widget slot holds an objectui component node, so its body is passthrough BaseSchema's (which is why value / icon / trend / trendValue, plugin-dashboard's registry inputs, survive), while every other widget is the spec-derived widget schema's.

Fenced to plugin-dashboard, deliberately. #4616's 35 unregistered non-dashboard gallery entries would light up under a catalog-wide gate; that is a different card with a different decision behind it. Reported, not built.

3. Counter-probe (the card's Zone 3 requirement). Seven mutants of a real entry, run through the sameauditEntry the real entries run, each naming which check must catch it — plus an unmutated clone that must stay clean, so a bug making the audit report on everything cannot make the probes pass vacuously.

Three real drifts the closure surfaced

Each is the same defect class as this card — a widget type no contract declares — and each was mechanical, with the correct form pinned by existing evidence. Named here rather than folded in silently:

  • plugin-designer/src/DashboardEditor.tsx offered a grid widget. It is not a spec family and not in @object-ui/types' own DASHBOARD_WIDGET_TYPES (the exported list WidgetConfigPanel already derives from) — it resolved through ComponentRegistry to the view grid and was refused at publish: designer saves what the server rejects. Removed; nothing pinned it (no test in that package referenced it, and PageDesigner's own grid palette entry is a different surface, untouched).
  • app-shell's widget inspector and the designer both wrote an unvalidated string from their select boxes. Both now resolve the DOM string against the list that rendered the options — no cast, no consumer-side tolerance: a value not in the palette writes nothing rather than storing a refused type.
  • p1-spec-alignment.test.ts pinned type: 'bar-chart' on a dataset-bound widget. bar-chart is a plugin-chartscomponent type; the spec's family is bar, and classifyWidgetType returns passthrough for it, so the fixture pinned a shape that can never render what it describes. Spelling corrected; its ADR-0021 assertions are unaffected.

Verification

All at a54b8bca4 (the pushed head).

  • Typecheck, 7 packages (types, plugin-dashboard, plugin-designer, app-shell, sdui-parser, core, schema-catalog) — exit 0, 0 errors. This is also the cross-package reverse verification: the plugin-designer and app-shell errors appeared only after @object-ui/types was rebuilt, proving consumers read the new .d.ts and not a cached one.
  • Testspackages/types/src/__tests__ + examples/schema-catalog/test: 65 files, 2240 passed. Plus plugin-dashboard + plugin-designer (88 files / 802) and app-shell's metadata-admin (195 files / 1982) green.
  • Lint — per-package lint on all four touched packages: exit 0, 0 errors (warnings are pre-existing). A full-repo eslint . --no-inline-config run (3623 files) was also completed in-cap; the single finding in a file this PR touches is at index.ts:1190, ~900 lines from any hunk here, and is an artefact of --no-inline-config suppressing that file's own eslint-disable pair.
  • Reverse verification (TS) — restoring type: 'bar-chart' gives error TS2322: Type '"bar-chart"' is not assignable to type 'DashboardWidgetTypeName | undefined'; restore is byte-identical (empty diffstat) and green again.
  • Ablation (the gate's teeth, on a real entry, not a synthetic one) — injecting object + categoryField + aggregate into filtered-dashboard.json turns the gate red, naming all three keys on the real entry. Mutation confirmed on disk by grep count on the injected text (categoryField 1, aggregate 1) before the run, and absence confirmed after (0) with an empty diffstat; restore leg green, 21/21. The script carried a trap … EXIT INT TERM restore throughout.
  • LedgerDashboardWidgetTypeSchema registered in zod-mirror-parity's EXCLUSIONS (a bare vocabulary with no .shape). One docblock line was reworded because that guard's spec-dependency scan attributes a trailing docblock to the preceding export, and naming the local Spec… alias there would have recorded DashboardWidgetLayoutSchema as spec-derived — false.

Out of scope, filed

#6002DashboardWidgetSchema silently DROPS every undeclared key (z.object() strip semantics), while keys the spec retired are refused by name with a tombstone. That asymmetry is why the pre-ADR-0021 keys validate clean today, and closing it (.strict() vs .passthrough()) is a published-contract decision needing a stored/designer population measurement first. This PR protects the corpus against it catalog-locally; it protects nothing else in the repo.

#4616 (35 unregistered gallery entries) is noted above and left alone.


Generated by Claude Code

…-card
`DashboardWidgetSchema.type` was `string` / `z.string()` — an unbounded hatch
that accepted typos, retired chart families, and component types nothing
registers. It is now closed: the spec's `ChartTypeSchema` families BY REFERENCE
plus two named objectui extension sets, `DASHBOARD_WIDGET_TYPE_EXTENSIONS`
(`list`, `custom`) and `DASHBOARD_COMPONENT_WIDGET_TYPES` (`metric-card`).
`metric-card` lands in objectui's own CLOSED component enum, explicitly not the
spec widget enum, per the maintainer ruling of 2026-08-14.
Adds the standing catalog gate for the plugin-dashboard entries against
objectui's own component schema, with a counter-probe that proves it bites.
Part of #4600
`DashboardWidgetTypeSchema` is a bare vocabulary with no `.shape`, so it joins
EXCLUSIONS beside the other enums rather than MIRRORS.
Also rewords one docblock line: `zod-mirror-parity`'s spec-dependency scan
attributes a trailing docblock to the PRECEDING export, so naming the local
`Spec…` alias there recorded `DashboardWidgetLayoutSchema` as spec-derived,
which is false. The prose now names the spec's own schema instead.
Part of #4600
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3232.6 KB3990.2 KB
Main entry chunk (gzip)153.6 KB350 KB
Entry fileindex-HNUK0YbM.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)504.91KB114.42KB
core (index.js)4.92KB1.97KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)165.30KB45.79KB
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.00KB61.86KB
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.10KB6.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 (index.js)4.13KB1.96KB
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 12:04
@yinlianghui
yinlianghui added this pull request to the merge queueAug 24, 2026
Merged via the queue into main with commit f7e34caAug 24, 2026
23 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4600-schema-catalog-dashboard-gate branch August 24, 2026 12:16
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@yinlianghui@claude