Skip to content

fix(types): widen seven zod mirrors the renderer already implements (#5927 group A) - #6032

Merged
yinlianghui merged 3 commits into
mainfrom
claude/issue-5927-zod-mirror-group-a-widenings
Aug 24, 2026
Merged

fix(types): widen seven zod mirrors the renderer already implements (#5927 group A)#6032
yinlianghui merged 3 commits into
mainfrom
claude/issue-5927-zod-mirror-group-a-widenings

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Refs #5927 — group A only. Groups B/C/D/E stay open, so this does not close the card.

What this is

Seven keys across five hand-written zod mirrors refused values the published
TypeScript declarations invite. That is declared != enforced on a published
validator (@object-ui/types/zod). The mirrors are widened to their declarations.

Direction: the contract WIDENS. Nothing is narrowed, nothing previously
accepted is now rejected. For consumers that means no schema that parsed before
can stop parsing; the only visible effect is that z.infer of these schemas
gains members, so code that exhaustively switches on an inferred union (a
switch over chartType with no default, say) will want the new arms. The
downstream sweep below is green on that basis, not by assumption.

Why every row was measured instead of matched

Per the maintainer ruling recorded on #5927 (2026-08-24, verbatim
「四维分析一致的,接手你的建议。」), widening every mirror to its declaration is
refused even though it is the cheap option: it makes each DISJOINT pair accept
both spellings, so an AI-authored app could write the dead one, pass
validation, and render nothing — a silent failure the validator itself
endorses. So each key here was decided by reading the running code, per the
#4605 precedent. A spelling that appears only in a type union does not count.

mirror key newly accepted the running code that implements it
navigation.zod.ts#ButtonGroupSchema variant secondary, destructive, ghost, link buttonVariants cva map, components/src/ui/button.tsx:19-27 — all six variants carry real class strings; the renderer passes button.variant || schema.variant straight to Button (renderers/basic/button-group.tsx:35)
navigation.zod.ts#ButtonGroupSchema size icon same cva map, size.icon = "h-10 w-10" (ui/button.tsx:33)
objectql.zod.ts#ObjectChartSchema chartType column, horizontal-bar, donut AdvancedChartImpl.tsx:405,1398,1480 normalize columnbar; :626 maps horizontal-bar to BarChart with :1228 isHorizontal and the :1333 layout branch; :630 maps donut to PieChart with :904 innerRadius = '52%'; plugin-charts/src/index.tsx:163-169 registers a donut-chart block
form.zod.ts#FormSchema validationMode onTouched, all renderers/form/form.tsx:1103 hands it verbatim to react-hook-form's useForm({ mode }); RHF implements both as real branches — isOnTouch: mode === VALIDATION_MODE.onTouched / isOnAll: mode === VALIDATION_MODE.all (react-hook-form@7.85.0/dist/index.esm.mjs:676-677) read at :2004 and :2007
form.zod.ts#SelectSchema defaultValue, value boolean renderers/form/select.tsx:48-50,62toControlValue(schema.defaultValue) stringifies a boolean to "true", SelectItem value={String(opt.value)} renders the matching option, and matchOptionValue maps it back to the authored true (option-value.ts, the #3090 round-trip whose OptionValue type is string | number | boolean)
data-display.zod.ts#DataTableSchema selectable 'single', 'multiple' resolveSelectionMode (renderers/complex/data-table.tsx:592) — 'single' is a real mode, not a truthy alias: :1215 replaces the selection set instead of extending it, and :1816 renders the select-all header only for 'multiple'
views.zod.ts#ViewSwitcherSchema (via ViewTypeSchema) defaultView, activeView, views[].type chart plugin-list/src/ListView.tsx:2224 and plugin-view/src/ObjectView.tsx:1228 both carry a case 'chart': that builds an object-chart node; plugin-list/src/ViewSwitcher.tsx:73,100 gives it an icon and a label key, and ListView.tsx:2040 switches on the value the switcher sets

No group A row measured dead, so this PR lands no narrowing — consistent
with the ruling that an ADR-0049 withdrawal is a separate breaking change.

One corroboration worth naming: ButtonGroupButtonSchema (navigation.zod.ts:144-145)
already carried the full six-variant / four-size vocabulary. Only the
group-level default was behind, and the renderer feeds both to the same
Button.

Re-measurement, not the card's table

The card's table is stale by construction and its own first comment says so.
So #5684's derived construction was re-run over all 163 registered pairs
rather than read off the table, by generating one never assignment per pair
against DriftOf< pair > and reading the pairs tsc names.

SPEC_DERIVED_PAIRS was checked before editing: none of the seven group A rows
is spec-derived (that set is AppComponentSchema, NavigationAreaSchema,
BaseSchema, DashboardComponentSchema, DashboardWidgetSchema,
SelectOptionSchema, PageNodeSchema, ObjectGanttSchema, ObjectMapSchema,
ObjectViewSchema), so no spec-bound mirror is widened here. Note
SelectOptionSchema is spec-derived, but the keys touched are
SelectSchema's own defaultValue/value, not the option's.

The ledger closes in the same PR

KnownDrift is shrink-only and pinned to each pair's exact drifted key set, so
a corrected drift fails the guard until its row is corrected. Four entries left
outright (SelectSchema, ButtonGroupSchema, ObjectChartSchema,
ViewSwitcherSchema); two shrank to the keys that are not widenings —
DataTableSchema keeps rowActions, FormSchema keeps fields/mode. Group
E (PageNodeSchema.pageType) is untouched and stays ledgered with its reason.

Non-vacuity, measured rather than assumed. With the widening in place, one
deleted row was put back and the ratchet turned red naming the exact pair:

src/__tests__/zod-mirror-parity.test.ts(567,14): error TS2322:
  Type '"objectql.zod.ts#ObjectChartSchema"' is not assignable to type 'never'.

So the row deletions are not masking a no-op widening.

Reverse verification

A probe authors one document per newly-admitted spelling and calls safeParse.
It imports the worktree source mirrors directly, not the package exports,
so a mutation on disk is visible with no rebuild step. Direction was predicted
before each run.

  • Before the edits: 17 refused, 0 of them parsing; 6 control cases
    (spellings both sides already admitted — variant: outline, chartType: bar,
    validationMode: onBlur, defaultValue: 1, selectable: true,
    defaultView: grid) all parsed. A probe that could not parse anything would
    look identical to a narrow mirror; the controls rule that out.
  • After: TOTAL parses=23 refused=0.
  • Per-mirror ablation: each widening was reverted alone, under
    trap ... EXIT INT TERM, and only its own rows went red — 4 / 1 / 3 / 2 / 2 /
    2 / 3 across the seven legs, with all six controls still parsing in every leg.
    Each mutation was proved on disk by grepping the injected text and
    separately
    the removed text before the reading was taken (an editor's exit
    code proves nothing on a zero-hit anchor). git diff HEAD --stat was empty
    afterwards.

Gates

Run on 956940b1b (the tree in this PR, merged with d2fb6efb4); exit codes
captured before any pipe.

gate exit
pnpm --filter @object-ui/types type-check (tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json — this is where the parity ratchet lives) 0
npx vitest run packages/types/src --maxWorkers=2 — 55 files, 606 tests passed 0
pnpm --workspace-concurrency=2 --filter '...@object-ui/types' type-check — the prefix form, i.e. downstream consumers; 42 projects Done 0
npx vitest run packages/plugin-view/src/__tests__/ViewSwitcher.test.tsx packages/plugin-charts/src packages/plugin-list/src --maxWorkers=2 — 82 files 0

The downstream sweep needed pnpm --filter '@object-ui/react-runtime...' build
first: @object-ui/react-runtime does not depend on @object-ui/types, so the
...@object-ui/types closure never builds it while packages/components
requires its dist. That is a property of the filter, not of this change.

Not in this PR

Groups B (6 disjoint-vocabulary rows) and C (2 required-vs-optional rows) are
write-up only under the ruling and go back to the decision inbox as one issue
with the renderer reading attached. Group D rides #2231. Group E stays
ledgered. #5853's opposite direction is untouched.


Generated by Claude Code

os-litant and others added 3 commits August 24, 2026 13:17
Group A of objectui#5927 — the strict-widening class, where the TS
declaration is a superset and the hand-written zod mirror is simply
behind. Each key was decided by MEASURING the renderer, per the #4605
precedent, not by matching the declaration.

Refs #5927
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 52 chunks) 3233.0 KB 3990.2 KB
Main entry chunk (gzip) 153.6 KB 350 KB
Entry file index-DBWcjTgz.js
Status PASS

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 10.38KB 3.90KB
app-shell (runtime-config.js) 18.10KB 6.51KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 505.23KB 114.56KB
core (index.js) 4.92KB 1.97KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 165.30KB 45.79KB
fields (index.js) 238.40KB 59.89KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.44KB 1.39KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 23.13KB 7.63KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 33.40KB 8.71KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 38.95KB 10.97KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.55KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 9.53KB 3.38KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 4.64KB 1.50KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 1.93KB 0.88KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.62KB 12.83KB
plugin-charts (index.js) 64.66KB 18.32KB
plugin-chatbot (index.js) 188.21KB 44.67KB
plugin-dashboard (index.js) 133.35KB 34.44KB
plugin-designer (index.js) 212.30KB 42.80KB
plugin-detail (index.js) 244.12KB 61.87KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 125.63KB 30.64KB
plugin-gantt (index.js) 164.15KB 39.88KB
plugin-grid (index.js) 200.79KB 54.26KB
plugin-kanban (index.js) 52.93KB 14.60KB
plugin-list (index.js) 111.86KB 27.22KB
plugin-map (index.js) 20.11KB 6.64KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.49KB 11.93KB
plugin-timeline (index.js) 26.49KB 7.59KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.57KB 20.74KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 52.40KB 17.45KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.35KB 0.70KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 12.13KB 3.65KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 7.54KB 2.63KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.74KB 1.41KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.49KB 2.14KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.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 14:08
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 24, 2026
Merged via the queue into main with commit 69a2163 Aug 24, 2026
23 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-5927-zod-mirror-group-a-widenings branch August 24, 2026 14:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants