fix(types): the 58 on* handler keys declared z.function() refuse BY NAME (objectui#6124) - #7339

Merged
os-litant merged 1 commit into
mainfrom
claude/issue-6124-on-handler-keys-refusal-arm
Sep 2, 2026
Merged

fix(types): the 58 on* handler keys declared z.function() refuse BY NAME (objectui#6124)#7339
os-litant merged 1 commit into
mainfrom
claude/issue-6124-on-handler-keys-refusal-arm

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes#6124

What this PR does

The @object-ui/types zod mirrors declared 58 on* handler keys (26 distinct) across 8 files as z.function() — a declaration no JSON document can satisfy on a JSON-authored vocabulary. A JSON author who wrote onClick: { "action": "toast" } was refused with zod's bare invalid_type … expected function, received object, which names the key and nothing else.

This PR executes the maintainer ruling of 2026-08-30 (director seat, batch #8, verbatim 「批次 #8 同意」, comment 5469346437 on #6124): Q1 → A, Q2 → A with C, Q3 → A, Q4 → B.

Clause ②: yes — the published validators' accept set moves (a function value that parsed green is now refused). Contract-review tier: this PR stays draft and carries needs:contract-review.

Census on the base (c93b4d5f3, re-run, not copied)

Anchored ^\s*on[A-Z][A-Za-z]*: z\.function\( over packages/types/src/zod/*.ts: 58 sites, 26 distinct keys, 8 files (complex 10, data-display 8, disclosure 3, feedback 1, form 20, layout 2, navigation 4, overlay 10). All key: z.function( sites: 62 = 58 + the four non-on* (cell, custom, renderCellEditor, validate). Raw z.function( occurrences: 64 = 62 + EventHandlersSchema + one mention inside a describe string. The card's 28/60 and the ruling's 29/61 are both stale; the close condition is this table.

Schema (file)KeyTS twinRuntime consumer measuredDisposition
KanbanSchema (complex)onCardMove, onCardClickcomplex.tsplugin-kanban/index.tsx:193-194 forwards schema.onCardMove / schema.onCardClickruntime slot
KanbanSchema (complex)onColumnAdd, onCardAddcomplex.tsrenderer takes ({ schema }), never reads themretired → ?: never
CalendarViewSchema (complex)onViewChangecomplex.tscalendar-view-renderer.tsx:319pickHostCallbacks(rest) (function values only) → CalendarViewruntime slot
FilterBuilderSchema (complex)onChangecomplex.tsfilter-builder.tsx:26 calls props.onChangeruntime slot
CarouselSchema (complex)onSlideChangecomplex.ts{...props} onto a div; CarouselProps has no such propretired
ChatbotSchema (complex)onError, onSendcomplex.tsplugin-chatbot/renderer.tsx:89,94useObjectChatruntime slot
ChatbotSchema (complex)onSendMessagecomplex.tsrenderer wires its own handleSendMessage; toDomProps drops the keyretired
AlertSchema (data-display)onDismissdata-display.ts{...props} spread onto the Alert element (no such event)retired
ListItemSchema (data-display)onClickListItemlist.tsx renders each item as a plain li with its contentretired
DataTableSchema (data-display)onRowEdit, onRowDelete, onSelectionChange, onColumnsReorderdata-display.tsdata-table.tsx:551,576,1341,1494 call them off schema.*runtime slot
TreeViewSchema (data-display)onSelectChange, onExpandChangedata-display.ts{...props} onto a div; nodes get only onNodeClickretired
AccordionSchema / CollapsibleSchema / ToggleGroupSchema (disclosure)onValueChange / onOpenChange / onValueChangedisclosure.tsleftover props spread onto the Radix root, where the prop is realruntime slot
ToastSchema (feedback)onDismissfeedback.tstoast.tsx takes ({ schema }), never reads itretired
ButtonSchema (form)onClickform.tsbutton.tsx:67toFormControlDomProps forwards it (onClickSDUI_DOM_PASS_THROUGH_KEYS)runtime slot
InputSchema, TextareaSchema, SelectSchema, CheckboxSchema, FileUploadSchema, DatePickerSchema, InputOTPSchema (form)onChangeform.tseach renderer destructures onChange from props and calls itruntime slot
RadioGroupSchema, SwitchSchema, ToggleSchema, SliderSchema, ComboboxSchema (form)onChangeform.tsonly toFormControlDomProps's whitelist is forwarded; no by-name readretired
CalendarSchema (form)onChangeform.ts{...props} onto DayPicker, whose callback is onSelectretired
CommandSchema (form)onChangeform.tslands on cmdk's root div as a DOM change listener (SyntheticEvent) — not the declared (value: string) => voidretired (leak noted)
InputOTPSchema (form)onCompleteform.tswhitelist drops itretired
FormSchema (form)onSubmit, onChange, onCancelform.tsform.tsx:1002-1005 destructures off schema; called at :2008, :1869, :2096runtime slot
CodeEditorSchema (form)onChangeform.tsplugin-editor/index.tsx:48onChange ?? schema.onChangeruntime slot
CardSchema (layout)onClicklayout.tscard.tsx:35,46 reads props.onClick, spreads onto the Card elementruntime slot
TabsSchema (layout)onValueChangelayout.tstabs.tsx:45{...tabsProps} after its own onValueChangeruntime slot
BreadcrumbItemSchema (navigation)onClickBreadcrumbItembreadcrumb.tsx renders links; never reads itretired
SidebarSchema (navigation)onCollapsedChangenavigation.ts{...props} spread onto the Sidebar component; no such propretired
PaginationSchema (navigation)onPageChangenavigation.tspagination.tsx:32 calls props.onPageChangeruntime slot
ButtonGroupButtonSchema (navigation)onClickButtonGroupButtonbutton-group.tsx renders each Button without a click handlerretired
DialogSchema, SheetSchema, DrawerSchema, PopoverSchema, HoverCardSchema, DropdownMenuSchema, AlertDialogSchema (overlay)onOpenChangeoverlay.ts{...props} onto the Radix / vaul root, where onOpenChange is realruntime slot
AlertDialogSchema (overlay)onConfirm, onCanceloverlay.tsfooter wired to schema.onAction and AlertDialogCancel; never readretired
MenuItemSchema (overlay)onClickMenuCommandItemdropdown-menu.tsx:77, context-menu.tsx:75, menubar.tsx:53,67 call item.onClick?.()runtime slot

Totals: 36 runtime slots + 22 retired = 58.

Parity ledger (zod-mirror-parity.test.ts) — every changed row

The mirror's z.input for a refusal arm is undefined, so a runtime-slot key (TS keeps the function) is a measured TYPE drift and lands in KnownDrift; a retired key (?: never) meets undefined on both sides and does not drift. 28 pairs, 35 keys (the 36th, MenuItemSchema.onClick, is a lazy union in EXCLUSIONS). Header counts re-read: 12 → 36 entries, 17 → 52 keys, 146 → 122 pairs with no entry; a class note above ButtonSchema explains the deliberate two-face divergence.

Extended (4): complex.zod.ts#ChatbotSchema: 'body' | 'onError' | 'onSend' · complex.zod.ts#FilterBuilderSchema: 'fields' | 'onChange' · data-display.zod.ts#DataTableSchema: 'rowActions' | 'onRowEdit' | 'onRowDelete' | 'onSelectionChange' | 'onColumnsReorder' · form.zod.ts#FormSchema: 'fields' | 'mode' | 'onSubmit' | 'onChange' | 'onCancel'.

New (24): complex.zod.ts#CalendarViewSchema: 'onViewChange' · complex.zod.ts#KanbanSchema: 'onCardMove' | 'onCardClick' · disclosure.zod.ts#AccordionSchema: 'onValueChange' · disclosure.zod.ts#CollapsibleSchema: 'onOpenChange' · disclosure.zod.ts#ToggleGroupSchema: 'onValueChange' · form.zod.ts#ButtonSchema: 'onClick' · form.zod.ts#CheckboxSchema: 'onChange' · form.zod.ts#CodeEditorSchema: 'onChange' · form.zod.ts#DatePickerSchema: 'onChange' · form.zod.ts#FileUploadSchema: 'onChange' · form.zod.ts#InputOTPSchema: 'onChange' · form.zod.ts#InputSchema: 'onChange' · form.zod.ts#SelectSchema: 'onChange' · form.zod.ts#TextareaSchema: 'onChange' · layout.zod.ts#CardSchema: 'onClick' · layout.zod.ts#TabsSchema: 'onValueChange' · navigation.zod.ts#PaginationSchema: 'onPageChange' · overlay.zod.ts#AlertDialogSchema: 'onOpenChange' · overlay.zod.ts#DialogSchema: 'onOpenChange' · overlay.zod.ts#DrawerSchema: 'onOpenChange' · overlay.zod.ts#DropdownMenuSchema: 'onOpenChange' · overlay.zod.ts#HoverCardSchema: 'onOpenChange' · overlay.zod.ts#PopoverSchema: 'onOpenChange' · overlay.zod.ts#SheetSchema: 'onOpenChange'.

Tests

New pin packages/types/src/__tests__/handler-keys-json-refusal-6124.test.ts (240 cases): anchored source census (0 on* sites; the 4 non-on* sites pinned by name), 58 × {declared on the shape with the #6124 guidance · authored object refused with code: 'custom', path [key], message naming the key, "type" and action:button, message === describe · live function refused · isolated shape green without the key}, wording split (RUNTIME SLOT vs RETIRED), the passthrough counter-probe (omit → green with the object kept), arm ≠ tombstone (custom vs invalid_type), and type-level pins: 22 × RetiredIsNever, 36 × KeepsFunction, both helpers shown able to fail. Compiled by tsconfig.test.jsontsc --listFiles lists the file (544 files).

Three existing tests restated for the accept-set change (verdicts unchanged, messages moved): component-fixture-declared-keys.test.ts counter-probe (still RED, now custom + named guidance), menu-item-union.test.ts (function parses on the TS face only; union arm errors dug), chatbot-authoring-face-keys.test.ts (onSend refused by name on the mirror).

Red-first on the unmodified tree (c93b4d5f3, pin file only): vitest Tests 178 failed | 62 passed (240) — census found 58 sites; object refused with invalid_type not custom; functions parsed GREEN; tsc -p tsconfig.test.json: 22 × TS2344 on RetiredIsNever exactly as predicted.

Union on the final commit 11fd53500 (all through os-verify-lock.sh, verdict lines quoted): pnpm --filter @object-ui/types run type-check (echoed tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json) type-check-exit=0 · pnpm exec vitest run packages/types/ examples/schema-catalog/test/component-fixture-declared-keys.test.ts --maxWorkers=2Test Files 88 passed (88) / Tests 1455 passed (1455) · pnpm --filter @object-ui/types build✓ dist completeness: 1 package(s) complete (118 emitted files verified) · downstream pnpm --filter @object-ui/core run type-check against the rebuilt dist (core's only workspace dep is @object-ui/types) → exit 0.

Ablation (after the commit; trap … EXIT INT TERM; absolute paths): ButtonSchema.onClick reverted to z.function().optional().describe('Click handler') — on-disk proof arm-before=1 mut-before=0 → arm-after=0 mut-after=1, git diff --stat 1/1; no build needed (the pin imports ../zod/form.zod from SOURCE). Result: Tests 6 failed | 234 passed (240) — census (1 site), Button declared/describe, Button object (invalid_type), Button function (GREEN), wording, arm-vs-tombstone — exactly the Button rows. Restore: git checkout HEAD -- ABS_PATH, git diff HEAD empty, git hash-object = git rev-parse HEAD:… = 23a63abe…, git status clean.

Gates (unlocked check:*, verdict lines): check-changeset-presence✅ 21 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s) · check-changeset-no-major✅ No changeset declares a major bump · check:control-bytes✅ OK (scanned 6023 tracked text file(s)) · check:spec-symbols✅ spec symbol derivation: 1333 files scanned · check:doc-types✅ Every documented component type is registered · check:vi-mock-inherit / check:vi-mock-specifiers✅ OK.

Lint, narrowed and measured:eslint --no-inline-config --format json over the 22 staged .ts files → population from eslint's own json output = 22 files, 0 errors, 54 warnings; every warning is @typescript-eslint/no-explicit-any and each warned file's merge-base blob lints to the identical E/W count (complex.ts 0/8, data-display.ts 0/30, form.ts 0/10, complex.zod.ts 0/2, data-display.zod.ts 0/1, navigation.zod.ts 0/2, overlay.zod.ts 0/1) — pre-existing; config invariance: eslint.config.js has no projectService / parserOptions.project, so this diff cannot move an untouched file's verdict. Repo-wide pnpm lint left to CI.

Downstream type-check, narrowed and declared: the only breaking TS change is the 22 ?: never members. A read census over packages/*/src, apps/*/src, examples, scripts for files naming both an affected interface and its retired key found three, none of which assigns the key on that type (AppHeader.tsx builds BreadcrumbItem[] segments without onClick; ObjectCalendar.tsx declares its own local CalendarSchema; chatbot renderer.tsx intersects ChatbotSchema with onSend/onClear, and onSendMessage there is the Chatbot component's own prop). @object-ui/core was type-checked for real against the rebuilt dist; the full turbo run type-check --filter='...@object-ui/types' needs the whole repo built and exceeds the foreground cap on a shared box — CI runs it.

Changeset

.changeset/6124-handler-keys-json-refusal.md@object-ui/types: minor (objectui refuses major); states the accept-set change plainly.

Not in this PR (for the PM)

🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…BY NAME (objectui#6124)
The zod mirrors declared 58 `on*` keys (26 distinct) across eight files as
`z.function()` — a declaration no JSON document can satisfy on a JSON-authored
vocabulary. A JSON author who wrote `onClick: { "action": "toast" }` got zod's
bare `invalid_type … expected function`, naming the key and nothing else.
Ruled shape (maintainer 2026-08-30, batch #8: Q1→A, Q2→A+C, Q3→A, Q4→B):
every site keeps its declaration and becomes a NAMED refusal arm in the #5099
`z.custom` + guidance shape (`handlerKeyRefusal()` in `zod/tombstone.zod.ts`):
the message names the key, says why JSON cannot author it, and points at the
node-type spelling PR #6498 established. The same string is the `.describe()`.
Deletion was measured and refused: under `BaseSchema.passthrough()` an
undeclared key is KEPT, and `onClick` rides `SDUI_DOM_PASS_THROUGH_KEYS` into
the DOM listener slot where React throws at click.
Accept-set change (Clause ②): a live function is refused on the mirror too.
Measured on this tree, the only runtime `safeParse` doors into these mirrors
are the CLI validators and `validateSchema` / `safeValidateSchema`;
`SchemaRenderer` validates through `@object-ui/core`'s structural validator.
TypeScript face, measured per key: `SchemaRenderer` spreads every non-metadata
schema key as a React prop, so 36 keys whose function value reaches a renderer
(read off `schema.*`, called as `props.onX`, or spread onto a Radix root / DOM
listener slot) keep their function type; 22 keys nothing reads carry the
`?: never` tombstone (ADR-0049). The parity ledger records the 35 registered
runtime-slot keys as deliberate drift (28 pairs; `MenuItemSchema` is a lazy
union outside the registry).
Out of scope per the ruling: `cell` / `custom` / `validate` /
`renderCellEditor` stay `z.function()`; `EventHandlersSchema` is objectui#6910.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRRumy89BYdW9ogbcdHTho
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.3 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-CljwiCD4.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.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.72KB2.24KB
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

@os-litant
os-litant marked this pull request as ready for review September 2, 2026 08:49
@os-litant
os-litant added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit a8d5c71Sep 2, 2026
32 checks passed
@os-litant
os-litant deleted the claude/issue-6124-on-handler-keys-refusal-arm branch September 2, 2026 09:04
os-litant added a commit that referenced this pull request Sep 2, 2026
…rable props
objectui#6124 (PR #7339) split the `on*` handler keys in two: 36 runtime
slots keep their function type, and 22 keys nothing reads became `?: never`
tombstones with named refusal arms on the zod mirror. Ten rows across eight
`content/docs` pages went on listing some of the retired 22 as callable
props — a key that is now a `tsc` error to assign and a refusal by name at
authoring time.
The card named three pages (`api/schema-reference.md`,
`data-display/tree-view.mdx`, `form/input-otp.mdx`, five rows); a whole-docs
census resolving each row's `(interface, key)` pair against the shipped
declaration found five more rows on five more pages —
`basic/button-group.mdx` (`ButtonGroupButton.onClick`), and `.onChange` on
`form/calendar.mdx`, `form/combobox.mdx`, `form/command.mdx`,
`form/radio-group.mdx`. A key NAME cannot decide this: `onChange` is retired
on 8 schemas and live on 14, and `input-otp.mdx` carries both dispositions
one line apart.
Seven pages have the retired rows REMOVED. `basic/button-group.mdx` is the
one "marked retired" page and not by taste: `button-group-doc-surface-6347`
asserts set equality between that page's `ButtonGroupButton` block and the
mirror's `.shape`, and a refusal arm is still a key of that shape, so the row
must stay — spelled `never`, with the node-type pointer the tombstone JSDoc
uses. That sibling pin's `onClick` case is updated to read the mirror's
refusal instead of restating the now-stale `() => void`.
The new pin measures the retired population off `packages/types/src` rather
than hand-listing it, flags rows (not prose), and carries six runtime-slot
rows as the blanket-sweep control — two of them on pages this change edits.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRRumy89BYdW9ogbcdHTho
os-project-manager pushed a commit that referenced this pull request Sep 3, 2026
…fuse BY NAME
The objectui#6182 ruling (2026-08-25, option A): the handler-expression string
dialect is not a supported authoring form, on either face. Executed in the
objectui#6124 shape (PR #7339) over the eight sites that PR's `z.function(`
census could not see: four `z.string()` mirrors, three `z.any()` mirrors and
one multi-line `z.function()`.
Per key, measured on origin/main d88e20f:
runtime slot (callable kept, mirror refuses by name):
views.zod.ts DetailViewSchema.onBack — detail-view spreads onto DetailView,
handleBack CALLS onBack(); TS twin
declared `string`, now `() => void`
crud.zod.ts DetailSchema.onBack — register('detail', DetailView)
crud.zod.ts ActionSchema.onClick — ActionRunner awaits action.onClick()
complex.zod.ts CalendarViewSchema.onEventClick — pickHostCallbacks
retired (`?: never`, mirror refuses by name):
app.zod.ts AppActionSchema.onClick — nothing reads AppComponentSchema.actions[]
reports.zod.ts ReportBuilderSchema.onSave / onCancel — no report-builder renderer
crud.zod.ts CRUDDialogSchema.onClose — no crud-dialog renderer
No key was read as a string and dispatched (the card's STOP class did not occur).
The three views.zod.ts event-NAME keys (PR #6899) are untouched; the new pin reads
their describe text as the reason they survive the census.
zod-mirror-parity.test.ts gains the three runtime-slot drift rows (39 entries /
56 keys, counted); component-docs-retired-handler-keys-7340.test.ts records the
ruled move of the `?: never` census (22 → 26) and the two names now retired
everywhere (`onClose`, `onSave`); phase2-schemas.test.ts keeps its old
string-callback fixture as the negative reading. content/docs/core/app-schema.mdx
carries the two edits the #7340 pin and check:doc-snippets require.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-litant@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(types): the 58 on* handler keys declared z.function() refuse BY NAME (objectui#6124) - #7339

Merged
os-litant merged 1 commit into
mainfrom
claude/issue-6124-on-handler-keys-refusal-arm
Sep 2, 2026
Merged

fix(types): the 58 on* handler keys declared z.function() refuse BY NAME (objectui#6124)#7339
os-litant merged 1 commit into
mainfrom
claude/issue-6124-on-handler-keys-refusal-arm

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes#6124

What this PR does

The @object-ui/types zod mirrors declared 58 on* handler keys (26 distinct) across 8 files as z.function() — a declaration no JSON document can satisfy on a JSON-authored vocabulary. A JSON author who wrote onClick: { "action": "toast" } was refused with zod's bare invalid_type … expected function, received object, which names the key and nothing else.

This PR executes the maintainer ruling of 2026-08-30 (director seat, batch #8, verbatim 「批次 #8 同意」, comment 5469346437 on #6124): Q1 → A, Q2 → A with C, Q3 → A, Q4 → B.

Clause ②: yes — the published validators' accept set moves (a function value that parsed green is now refused). Contract-review tier: this PR stays draft and carries needs:contract-review.

Census on the base (c93b4d5f3, re-run, not copied)

Anchored ^\s*on[A-Z][A-Za-z]*: z\.function\( over packages/types/src/zod/*.ts: 58 sites, 26 distinct keys, 8 files (complex 10, data-display 8, disclosure 3, feedback 1, form 20, layout 2, navigation 4, overlay 10). All key: z.function( sites: 62 = 58 + the four non-on* (cell, custom, renderCellEditor, validate). Raw z.function( occurrences: 64 = 62 + EventHandlersSchema + one mention inside a describe string. The card's 28/60 and the ruling's 29/61 are both stale; the close condition is this table.

Schema (file)KeyTS twinRuntime consumer measuredDisposition
KanbanSchema (complex)onCardMove, onCardClickcomplex.tsplugin-kanban/index.tsx:193-194 forwards schema.onCardMove / schema.onCardClickruntime slot
KanbanSchema (complex)onColumnAdd, onCardAddcomplex.tsrenderer takes ({ schema }), never reads themretired → ?: never
CalendarViewSchema (complex)onViewChangecomplex.tscalendar-view-renderer.tsx:319pickHostCallbacks(rest) (function values only) → CalendarViewruntime slot
FilterBuilderSchema (complex)onChangecomplex.tsfilter-builder.tsx:26 calls props.onChangeruntime slot
CarouselSchema (complex)onSlideChangecomplex.ts{...props} onto a div; CarouselProps has no such propretired
ChatbotSchema (complex)onError, onSendcomplex.tsplugin-chatbot/renderer.tsx:89,94useObjectChatruntime slot
ChatbotSchema (complex)onSendMessagecomplex.tsrenderer wires its own handleSendMessage; toDomProps drops the keyretired
AlertSchema (data-display)onDismissdata-display.ts{...props} spread onto the Alert element (no such event)retired
ListItemSchema (data-display)onClickListItemlist.tsx renders each item as a plain li with its contentretired
DataTableSchema (data-display)onRowEdit, onRowDelete, onSelectionChange, onColumnsReorderdata-display.tsdata-table.tsx:551,576,1341,1494 call them off schema.*runtime slot
TreeViewSchema (data-display)onSelectChange, onExpandChangedata-display.ts{...props} onto a div; nodes get only onNodeClickretired
AccordionSchema / CollapsibleSchema / ToggleGroupSchema (disclosure)onValueChange / onOpenChange / onValueChangedisclosure.tsleftover props spread onto the Radix root, where the prop is realruntime slot
ToastSchema (feedback)onDismissfeedback.tstoast.tsx takes ({ schema }), never reads itretired
ButtonSchema (form)onClickform.tsbutton.tsx:67toFormControlDomProps forwards it (onClickSDUI_DOM_PASS_THROUGH_KEYS)runtime slot
InputSchema, TextareaSchema, SelectSchema, CheckboxSchema, FileUploadSchema, DatePickerSchema, InputOTPSchema (form)onChangeform.tseach renderer destructures onChange from props and calls itruntime slot
RadioGroupSchema, SwitchSchema, ToggleSchema, SliderSchema, ComboboxSchema (form)onChangeform.tsonly toFormControlDomProps's whitelist is forwarded; no by-name readretired
CalendarSchema (form)onChangeform.ts{...props} onto DayPicker, whose callback is onSelectretired
CommandSchema (form)onChangeform.tslands on cmdk's root div as a DOM change listener (SyntheticEvent) — not the declared (value: string) => voidretired (leak noted)
InputOTPSchema (form)onCompleteform.tswhitelist drops itretired
FormSchema (form)onSubmit, onChange, onCancelform.tsform.tsx:1002-1005 destructures off schema; called at :2008, :1869, :2096runtime slot
CodeEditorSchema (form)onChangeform.tsplugin-editor/index.tsx:48onChange ?? schema.onChangeruntime slot
CardSchema (layout)onClicklayout.tscard.tsx:35,46 reads props.onClick, spreads onto the Card elementruntime slot
TabsSchema (layout)onValueChangelayout.tstabs.tsx:45{...tabsProps} after its own onValueChangeruntime slot
BreadcrumbItemSchema (navigation)onClickBreadcrumbItembreadcrumb.tsx renders links; never reads itretired
SidebarSchema (navigation)onCollapsedChangenavigation.ts{...props} spread onto the Sidebar component; no such propretired
PaginationSchema (navigation)onPageChangenavigation.tspagination.tsx:32 calls props.onPageChangeruntime slot
ButtonGroupButtonSchema (navigation)onClickButtonGroupButtonbutton-group.tsx renders each Button without a click handlerretired
DialogSchema, SheetSchema, DrawerSchema, PopoverSchema, HoverCardSchema, DropdownMenuSchema, AlertDialogSchema (overlay)onOpenChangeoverlay.ts{...props} onto the Radix / vaul root, where onOpenChange is realruntime slot
AlertDialogSchema (overlay)onConfirm, onCanceloverlay.tsfooter wired to schema.onAction and AlertDialogCancel; never readretired
MenuItemSchema (overlay)onClickMenuCommandItemdropdown-menu.tsx:77, context-menu.tsx:75, menubar.tsx:53,67 call item.onClick?.()runtime slot

Totals: 36 runtime slots + 22 retired = 58.

Parity ledger (zod-mirror-parity.test.ts) — every changed row

The mirror's z.input for a refusal arm is undefined, so a runtime-slot key (TS keeps the function) is a measured TYPE drift and lands in KnownDrift; a retired key (?: never) meets undefined on both sides and does not drift. 28 pairs, 35 keys (the 36th, MenuItemSchema.onClick, is a lazy union in EXCLUSIONS). Header counts re-read: 12 → 36 entries, 17 → 52 keys, 146 → 122 pairs with no entry; a class note above ButtonSchema explains the deliberate two-face divergence.

Extended (4): complex.zod.ts#ChatbotSchema: 'body' | 'onError' | 'onSend' · complex.zod.ts#FilterBuilderSchema: 'fields' | 'onChange' · data-display.zod.ts#DataTableSchema: 'rowActions' | 'onRowEdit' | 'onRowDelete' | 'onSelectionChange' | 'onColumnsReorder' · form.zod.ts#FormSchema: 'fields' | 'mode' | 'onSubmit' | 'onChange' | 'onCancel'.

New (24): complex.zod.ts#CalendarViewSchema: 'onViewChange' · complex.zod.ts#KanbanSchema: 'onCardMove' | 'onCardClick' · disclosure.zod.ts#AccordionSchema: 'onValueChange' · disclosure.zod.ts#CollapsibleSchema: 'onOpenChange' · disclosure.zod.ts#ToggleGroupSchema: 'onValueChange' · form.zod.ts#ButtonSchema: 'onClick' · form.zod.ts#CheckboxSchema: 'onChange' · form.zod.ts#CodeEditorSchema: 'onChange' · form.zod.ts#DatePickerSchema: 'onChange' · form.zod.ts#FileUploadSchema: 'onChange' · form.zod.ts#InputOTPSchema: 'onChange' · form.zod.ts#InputSchema: 'onChange' · form.zod.ts#SelectSchema: 'onChange' · form.zod.ts#TextareaSchema: 'onChange' · layout.zod.ts#CardSchema: 'onClick' · layout.zod.ts#TabsSchema: 'onValueChange' · navigation.zod.ts#PaginationSchema: 'onPageChange' · overlay.zod.ts#AlertDialogSchema: 'onOpenChange' · overlay.zod.ts#DialogSchema: 'onOpenChange' · overlay.zod.ts#DrawerSchema: 'onOpenChange' · overlay.zod.ts#DropdownMenuSchema: 'onOpenChange' · overlay.zod.ts#HoverCardSchema: 'onOpenChange' · overlay.zod.ts#PopoverSchema: 'onOpenChange' · overlay.zod.ts#SheetSchema: 'onOpenChange'.

Tests

New pin packages/types/src/__tests__/handler-keys-json-refusal-6124.test.ts (240 cases): anchored source census (0 on* sites; the 4 non-on* sites pinned by name), 58 × {declared on the shape with the #6124 guidance · authored object refused with code: 'custom', path [key], message naming the key, "type" and action:button, message === describe · live function refused · isolated shape green without the key}, wording split (RUNTIME SLOT vs RETIRED), the passthrough counter-probe (omit → green with the object kept), arm ≠ tombstone (custom vs invalid_type), and type-level pins: 22 × RetiredIsNever, 36 × KeepsFunction, both helpers shown able to fail. Compiled by tsconfig.test.jsontsc --listFiles lists the file (544 files).

Three existing tests restated for the accept-set change (verdicts unchanged, messages moved): component-fixture-declared-keys.test.ts counter-probe (still RED, now custom + named guidance), menu-item-union.test.ts (function parses on the TS face only; union arm errors dug), chatbot-authoring-face-keys.test.ts (onSend refused by name on the mirror).

Red-first on the unmodified tree (c93b4d5f3, pin file only): vitest Tests 178 failed | 62 passed (240) — census found 58 sites; object refused with invalid_type not custom; functions parsed GREEN; tsc -p tsconfig.test.json: 22 × TS2344 on RetiredIsNever exactly as predicted.

Union on the final commit 11fd53500 (all through os-verify-lock.sh, verdict lines quoted): pnpm --filter @object-ui/types run type-check (echoed tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json) type-check-exit=0 · pnpm exec vitest run packages/types/ examples/schema-catalog/test/component-fixture-declared-keys.test.ts --maxWorkers=2Test Files 88 passed (88) / Tests 1455 passed (1455) · pnpm --filter @object-ui/types build✓ dist completeness: 1 package(s) complete (118 emitted files verified) · downstream pnpm --filter @object-ui/core run type-check against the rebuilt dist (core's only workspace dep is @object-ui/types) → exit 0.

Ablation (after the commit; trap … EXIT INT TERM; absolute paths): ButtonSchema.onClick reverted to z.function().optional().describe('Click handler') — on-disk proof arm-before=1 mut-before=0 → arm-after=0 mut-after=1, git diff --stat 1/1; no build needed (the pin imports ../zod/form.zod from SOURCE). Result: Tests 6 failed | 234 passed (240) — census (1 site), Button declared/describe, Button object (invalid_type), Button function (GREEN), wording, arm-vs-tombstone — exactly the Button rows. Restore: git checkout HEAD -- ABS_PATH, git diff HEAD empty, git hash-object = git rev-parse HEAD:… = 23a63abe…, git status clean.

Gates (unlocked check:*, verdict lines): check-changeset-presence✅ 21 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s) · check-changeset-no-major✅ No changeset declares a major bump · check:control-bytes✅ OK (scanned 6023 tracked text file(s)) · check:spec-symbols✅ spec symbol derivation: 1333 files scanned · check:doc-types✅ Every documented component type is registered · check:vi-mock-inherit / check:vi-mock-specifiers✅ OK.

Lint, narrowed and measured:eslint --no-inline-config --format json over the 22 staged .ts files → population from eslint's own json output = 22 files, 0 errors, 54 warnings; every warning is @typescript-eslint/no-explicit-any and each warned file's merge-base blob lints to the identical E/W count (complex.ts 0/8, data-display.ts 0/30, form.ts 0/10, complex.zod.ts 0/2, data-display.zod.ts 0/1, navigation.zod.ts 0/2, overlay.zod.ts 0/1) — pre-existing; config invariance: eslint.config.js has no projectService / parserOptions.project, so this diff cannot move an untouched file's verdict. Repo-wide pnpm lint left to CI.

Downstream type-check, narrowed and declared: the only breaking TS change is the 22 ?: never members. A read census over packages/*/src, apps/*/src, examples, scripts for files naming both an affected interface and its retired key found three, none of which assigns the key on that type (AppHeader.tsx builds BreadcrumbItem[] segments without onClick; ObjectCalendar.tsx declares its own local CalendarSchema; chatbot renderer.tsx intersects ChatbotSchema with onSend/onClear, and onSendMessage there is the Chatbot component's own prop). @object-ui/core was type-checked for real against the rebuilt dist; the full turbo run type-check --filter='...@object-ui/types' needs the whole repo built and exceeds the foreground cap on a shared box — CI runs it.

Changeset

.changeset/6124-handler-keys-json-refusal.md@object-ui/types: minor (objectui refuses major); states the accept-set change plainly.

Not in this PR (for the PM)

🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…BY NAME (objectui#6124)
The zod mirrors declared 58 `on*` keys (26 distinct) across eight files as
`z.function()` — a declaration no JSON document can satisfy on a JSON-authored
vocabulary. A JSON author who wrote `onClick: { "action": "toast" }` got zod's
bare `invalid_type … expected function`, naming the key and nothing else.
Ruled shape (maintainer 2026-08-30, batch #8: Q1→A, Q2→A+C, Q3→A, Q4→B):
every site keeps its declaration and becomes a NAMED refusal arm in the #5099
`z.custom` + guidance shape (`handlerKeyRefusal()` in `zod/tombstone.zod.ts`):
the message names the key, says why JSON cannot author it, and points at the
node-type spelling PR #6498 established. The same string is the `.describe()`.
Deletion was measured and refused: under `BaseSchema.passthrough()` an
undeclared key is KEPT, and `onClick` rides `SDUI_DOM_PASS_THROUGH_KEYS` into
the DOM listener slot where React throws at click.
Accept-set change (Clause ②): a live function is refused on the mirror too.
Measured on this tree, the only runtime `safeParse` doors into these mirrors
are the CLI validators and `validateSchema` / `safeValidateSchema`;
`SchemaRenderer` validates through `@object-ui/core`'s structural validator.
TypeScript face, measured per key: `SchemaRenderer` spreads every non-metadata
schema key as a React prop, so 36 keys whose function value reaches a renderer
(read off `schema.*`, called as `props.onX`, or spread onto a Radix root / DOM
listener slot) keep their function type; 22 keys nothing reads carry the
`?: never` tombstone (ADR-0049). The parity ledger records the 35 registered
runtime-slot keys as deliberate drift (28 pairs; `MenuItemSchema` is a lazy
union outside the registry).
Out of scope per the ruling: `cell` / `custom` / `validate` /
`renderCellEditor` stay `z.function()`; `EventHandlersSchema` is objectui#6910.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRRumy89BYdW9ogbcdHTho
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.3 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-CljwiCD4.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.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.72KB2.24KB
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

@os-litant
os-litant marked this pull request as ready for review September 2, 2026 08:49
@os-litant
os-litant added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit a8d5c71Sep 2, 2026
32 checks passed
@os-litant
os-litant deleted the claude/issue-6124-on-handler-keys-refusal-arm branch September 2, 2026 09:04
os-litant added a commit that referenced this pull request Sep 2, 2026
…rable props
objectui#6124 (PR #7339) split the `on*` handler keys in two: 36 runtime
slots keep their function type, and 22 keys nothing reads became `?: never`
tombstones with named refusal arms on the zod mirror. Ten rows across eight
`content/docs` pages went on listing some of the retired 22 as callable
props — a key that is now a `tsc` error to assign and a refusal by name at
authoring time.
The card named three pages (`api/schema-reference.md`,
`data-display/tree-view.mdx`, `form/input-otp.mdx`, five rows); a whole-docs
census resolving each row's `(interface, key)` pair against the shipped
declaration found five more rows on five more pages —
`basic/button-group.mdx` (`ButtonGroupButton.onClick`), and `.onChange` on
`form/calendar.mdx`, `form/combobox.mdx`, `form/command.mdx`,
`form/radio-group.mdx`. A key NAME cannot decide this: `onChange` is retired
on 8 schemas and live on 14, and `input-otp.mdx` carries both dispositions
one line apart.
Seven pages have the retired rows REMOVED. `basic/button-group.mdx` is the
one "marked retired" page and not by taste: `button-group-doc-surface-6347`
asserts set equality between that page's `ButtonGroupButton` block and the
mirror's `.shape`, and a refusal arm is still a key of that shape, so the row
must stay — spelled `never`, with the node-type pointer the tombstone JSDoc
uses. That sibling pin's `onClick` case is updated to read the mirror's
refusal instead of restating the now-stale `() => void`.
The new pin measures the retired population off `packages/types/src` rather
than hand-listing it, flags rows (not prose), and carries six runtime-slot
rows as the blanket-sweep control — two of them on pages this change edits.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRRumy89BYdW9ogbcdHTho
os-project-manager pushed a commit that referenced this pull request Sep 3, 2026
…fuse BY NAME
The objectui#6182 ruling (2026-08-25, option A): the handler-expression string
dialect is not a supported authoring form, on either face. Executed in the
objectui#6124 shape (PR #7339) over the eight sites that PR's `z.function(`
census could not see: four `z.string()` mirrors, three `z.any()` mirrors and
one multi-line `z.function()`.
Per key, measured on origin/main d88e20f:
runtime slot (callable kept, mirror refuses by name):
views.zod.ts DetailViewSchema.onBack — detail-view spreads onto DetailView,
handleBack CALLS onBack(); TS twin
declared `string`, now `() => void`
crud.zod.ts DetailSchema.onBack — register('detail', DetailView)
crud.zod.ts ActionSchema.onClick — ActionRunner awaits action.onClick()
complex.zod.ts CalendarViewSchema.onEventClick — pickHostCallbacks
retired (`?: never`, mirror refuses by name):
app.zod.ts AppActionSchema.onClick — nothing reads AppComponentSchema.actions[]
reports.zod.ts ReportBuilderSchema.onSave / onCancel — no report-builder renderer
crud.zod.ts CRUDDialogSchema.onClose — no crud-dialog renderer
No key was read as a string and dispatched (the card's STOP class did not occur).
The three views.zod.ts event-NAME keys (PR #6899) are untouched; the new pin reads
their describe text as the reason they survive the census.
zod-mirror-parity.test.ts gains the three runtime-slot drift rows (39 entries /
56 keys, counted); component-docs-retired-handler-keys-7340.test.ts records the
ruled move of the `?: never` census (22 → 26) and the two names now retired
everywhere (`onClose`, `onSave`); phase2-schemas.test.ts keeps its old
string-callback fixture as the negative reading. content/docs/core/app-schema.mdx
carries the two edits the #7340 pin and check:doc-snippets require.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-litant@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(types): the 58 on* handler keys declared z.function() refuse BY NAME (objectui#6124) - #7339

Merged
os-litant merged 1 commit into
mainfrom
claude/issue-6124-on-handler-keys-refusal-arm
Sep 2, 2026
Merged

fix(types): the 58 on* handler keys declared z.function() refuse BY NAME (objectui#6124)#7339
os-litant merged 1 commit into
mainfrom
claude/issue-6124-on-handler-keys-refusal-arm

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes#6124

What this PR does

The @object-ui/types zod mirrors declared 58 on* handler keys (26 distinct) across 8 files as z.function() — a declaration no JSON document can satisfy on a JSON-authored vocabulary. A JSON author who wrote onClick: { "action": "toast" } was refused with zod's bare invalid_type … expected function, received object, which names the key and nothing else.

This PR executes the maintainer ruling of 2026-08-30 (director seat, batch #8, verbatim 「批次 #8 同意」, comment 5469346437 on #6124): Q1 → A, Q2 → A with C, Q3 → A, Q4 → B.

Clause ②: yes — the published validators' accept set moves (a function value that parsed green is now refused). Contract-review tier: this PR stays draft and carries needs:contract-review.

Census on the base (c93b4d5f3, re-run, not copied)

Anchored ^\s*on[A-Z][A-Za-z]*: z\.function\( over packages/types/src/zod/*.ts: 58 sites, 26 distinct keys, 8 files (complex 10, data-display 8, disclosure 3, feedback 1, form 20, layout 2, navigation 4, overlay 10). All key: z.function( sites: 62 = 58 + the four non-on* (cell, custom, renderCellEditor, validate). Raw z.function( occurrences: 64 = 62 + EventHandlersSchema + one mention inside a describe string. The card's 28/60 and the ruling's 29/61 are both stale; the close condition is this table.

Schema (file)KeyTS twinRuntime consumer measuredDisposition
KanbanSchema (complex)onCardMove, onCardClickcomplex.tsplugin-kanban/index.tsx:193-194 forwards schema.onCardMove / schema.onCardClickruntime slot
KanbanSchema (complex)onColumnAdd, onCardAddcomplex.tsrenderer takes ({ schema }), never reads themretired → ?: never
CalendarViewSchema (complex)onViewChangecomplex.tscalendar-view-renderer.tsx:319pickHostCallbacks(rest) (function values only) → CalendarViewruntime slot
FilterBuilderSchema (complex)onChangecomplex.tsfilter-builder.tsx:26 calls props.onChangeruntime slot
CarouselSchema (complex)onSlideChangecomplex.ts{...props} onto a div; CarouselProps has no such propretired
ChatbotSchema (complex)onError, onSendcomplex.tsplugin-chatbot/renderer.tsx:89,94useObjectChatruntime slot
ChatbotSchema (complex)onSendMessagecomplex.tsrenderer wires its own handleSendMessage; toDomProps drops the keyretired
AlertSchema (data-display)onDismissdata-display.ts{...props} spread onto the Alert element (no such event)retired
ListItemSchema (data-display)onClickListItemlist.tsx renders each item as a plain li with its contentretired
DataTableSchema (data-display)onRowEdit, onRowDelete, onSelectionChange, onColumnsReorderdata-display.tsdata-table.tsx:551,576,1341,1494 call them off schema.*runtime slot
TreeViewSchema (data-display)onSelectChange, onExpandChangedata-display.ts{...props} onto a div; nodes get only onNodeClickretired
AccordionSchema / CollapsibleSchema / ToggleGroupSchema (disclosure)onValueChange / onOpenChange / onValueChangedisclosure.tsleftover props spread onto the Radix root, where the prop is realruntime slot
ToastSchema (feedback)onDismissfeedback.tstoast.tsx takes ({ schema }), never reads itretired
ButtonSchema (form)onClickform.tsbutton.tsx:67toFormControlDomProps forwards it (onClickSDUI_DOM_PASS_THROUGH_KEYS)runtime slot
InputSchema, TextareaSchema, SelectSchema, CheckboxSchema, FileUploadSchema, DatePickerSchema, InputOTPSchema (form)onChangeform.tseach renderer destructures onChange from props and calls itruntime slot
RadioGroupSchema, SwitchSchema, ToggleSchema, SliderSchema, ComboboxSchema (form)onChangeform.tsonly toFormControlDomProps's whitelist is forwarded; no by-name readretired
CalendarSchema (form)onChangeform.ts{...props} onto DayPicker, whose callback is onSelectretired
CommandSchema (form)onChangeform.tslands on cmdk's root div as a DOM change listener (SyntheticEvent) — not the declared (value: string) => voidretired (leak noted)
InputOTPSchema (form)onCompleteform.tswhitelist drops itretired
FormSchema (form)onSubmit, onChange, onCancelform.tsform.tsx:1002-1005 destructures off schema; called at :2008, :1869, :2096runtime slot
CodeEditorSchema (form)onChangeform.tsplugin-editor/index.tsx:48onChange ?? schema.onChangeruntime slot
CardSchema (layout)onClicklayout.tscard.tsx:35,46 reads props.onClick, spreads onto the Card elementruntime slot
TabsSchema (layout)onValueChangelayout.tstabs.tsx:45{...tabsProps} after its own onValueChangeruntime slot
BreadcrumbItemSchema (navigation)onClickBreadcrumbItembreadcrumb.tsx renders links; never reads itretired
SidebarSchema (navigation)onCollapsedChangenavigation.ts{...props} spread onto the Sidebar component; no such propretired
PaginationSchema (navigation)onPageChangenavigation.tspagination.tsx:32 calls props.onPageChangeruntime slot
ButtonGroupButtonSchema (navigation)onClickButtonGroupButtonbutton-group.tsx renders each Button without a click handlerretired
DialogSchema, SheetSchema, DrawerSchema, PopoverSchema, HoverCardSchema, DropdownMenuSchema, AlertDialogSchema (overlay)onOpenChangeoverlay.ts{...props} onto the Radix / vaul root, where onOpenChange is realruntime slot
AlertDialogSchema (overlay)onConfirm, onCanceloverlay.tsfooter wired to schema.onAction and AlertDialogCancel; never readretired
MenuItemSchema (overlay)onClickMenuCommandItemdropdown-menu.tsx:77, context-menu.tsx:75, menubar.tsx:53,67 call item.onClick?.()runtime slot

Totals: 36 runtime slots + 22 retired = 58.

Parity ledger (zod-mirror-parity.test.ts) — every changed row

The mirror's z.input for a refusal arm is undefined, so a runtime-slot key (TS keeps the function) is a measured TYPE drift and lands in KnownDrift; a retired key (?: never) meets undefined on both sides and does not drift. 28 pairs, 35 keys (the 36th, MenuItemSchema.onClick, is a lazy union in EXCLUSIONS). Header counts re-read: 12 → 36 entries, 17 → 52 keys, 146 → 122 pairs with no entry; a class note above ButtonSchema explains the deliberate two-face divergence.

Extended (4): complex.zod.ts#ChatbotSchema: 'body' | 'onError' | 'onSend' · complex.zod.ts#FilterBuilderSchema: 'fields' | 'onChange' · data-display.zod.ts#DataTableSchema: 'rowActions' | 'onRowEdit' | 'onRowDelete' | 'onSelectionChange' | 'onColumnsReorder' · form.zod.ts#FormSchema: 'fields' | 'mode' | 'onSubmit' | 'onChange' | 'onCancel'.

New (24): complex.zod.ts#CalendarViewSchema: 'onViewChange' · complex.zod.ts#KanbanSchema: 'onCardMove' | 'onCardClick' · disclosure.zod.ts#AccordionSchema: 'onValueChange' · disclosure.zod.ts#CollapsibleSchema: 'onOpenChange' · disclosure.zod.ts#ToggleGroupSchema: 'onValueChange' · form.zod.ts#ButtonSchema: 'onClick' · form.zod.ts#CheckboxSchema: 'onChange' · form.zod.ts#CodeEditorSchema: 'onChange' · form.zod.ts#DatePickerSchema: 'onChange' · form.zod.ts#FileUploadSchema: 'onChange' · form.zod.ts#InputOTPSchema: 'onChange' · form.zod.ts#InputSchema: 'onChange' · form.zod.ts#SelectSchema: 'onChange' · form.zod.ts#TextareaSchema: 'onChange' · layout.zod.ts#CardSchema: 'onClick' · layout.zod.ts#TabsSchema: 'onValueChange' · navigation.zod.ts#PaginationSchema: 'onPageChange' · overlay.zod.ts#AlertDialogSchema: 'onOpenChange' · overlay.zod.ts#DialogSchema: 'onOpenChange' · overlay.zod.ts#DrawerSchema: 'onOpenChange' · overlay.zod.ts#DropdownMenuSchema: 'onOpenChange' · overlay.zod.ts#HoverCardSchema: 'onOpenChange' · overlay.zod.ts#PopoverSchema: 'onOpenChange' · overlay.zod.ts#SheetSchema: 'onOpenChange'.

Tests

New pin packages/types/src/__tests__/handler-keys-json-refusal-6124.test.ts (240 cases): anchored source census (0 on* sites; the 4 non-on* sites pinned by name), 58 × {declared on the shape with the #6124 guidance · authored object refused with code: 'custom', path [key], message naming the key, "type" and action:button, message === describe · live function refused · isolated shape green without the key}, wording split (RUNTIME SLOT vs RETIRED), the passthrough counter-probe (omit → green with the object kept), arm ≠ tombstone (custom vs invalid_type), and type-level pins: 22 × RetiredIsNever, 36 × KeepsFunction, both helpers shown able to fail. Compiled by tsconfig.test.jsontsc --listFiles lists the file (544 files).

Three existing tests restated for the accept-set change (verdicts unchanged, messages moved): component-fixture-declared-keys.test.ts counter-probe (still RED, now custom + named guidance), menu-item-union.test.ts (function parses on the TS face only; union arm errors dug), chatbot-authoring-face-keys.test.ts (onSend refused by name on the mirror).

Red-first on the unmodified tree (c93b4d5f3, pin file only): vitest Tests 178 failed | 62 passed (240) — census found 58 sites; object refused with invalid_type not custom; functions parsed GREEN; tsc -p tsconfig.test.json: 22 × TS2344 on RetiredIsNever exactly as predicted.

Union on the final commit 11fd53500 (all through os-verify-lock.sh, verdict lines quoted): pnpm --filter @object-ui/types run type-check (echoed tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json) type-check-exit=0 · pnpm exec vitest run packages/types/ examples/schema-catalog/test/component-fixture-declared-keys.test.ts --maxWorkers=2Test Files 88 passed (88) / Tests 1455 passed (1455) · pnpm --filter @object-ui/types build✓ dist completeness: 1 package(s) complete (118 emitted files verified) · downstream pnpm --filter @object-ui/core run type-check against the rebuilt dist (core's only workspace dep is @object-ui/types) → exit 0.

Ablation (after the commit; trap … EXIT INT TERM; absolute paths): ButtonSchema.onClick reverted to z.function().optional().describe('Click handler') — on-disk proof arm-before=1 mut-before=0 → arm-after=0 mut-after=1, git diff --stat 1/1; no build needed (the pin imports ../zod/form.zod from SOURCE). Result: Tests 6 failed | 234 passed (240) — census (1 site), Button declared/describe, Button object (invalid_type), Button function (GREEN), wording, arm-vs-tombstone — exactly the Button rows. Restore: git checkout HEAD -- ABS_PATH, git diff HEAD empty, git hash-object = git rev-parse HEAD:… = 23a63abe…, git status clean.

Gates (unlocked check:*, verdict lines): check-changeset-presence✅ 21 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s) · check-changeset-no-major✅ No changeset declares a major bump · check:control-bytes✅ OK (scanned 6023 tracked text file(s)) · check:spec-symbols✅ spec symbol derivation: 1333 files scanned · check:doc-types✅ Every documented component type is registered · check:vi-mock-inherit / check:vi-mock-specifiers✅ OK.

Lint, narrowed and measured:eslint --no-inline-config --format json over the 22 staged .ts files → population from eslint's own json output = 22 files, 0 errors, 54 warnings; every warning is @typescript-eslint/no-explicit-any and each warned file's merge-base blob lints to the identical E/W count (complex.ts 0/8, data-display.ts 0/30, form.ts 0/10, complex.zod.ts 0/2, data-display.zod.ts 0/1, navigation.zod.ts 0/2, overlay.zod.ts 0/1) — pre-existing; config invariance: eslint.config.js has no projectService / parserOptions.project, so this diff cannot move an untouched file's verdict. Repo-wide pnpm lint left to CI.

Downstream type-check, narrowed and declared: the only breaking TS change is the 22 ?: never members. A read census over packages/*/src, apps/*/src, examples, scripts for files naming both an affected interface and its retired key found three, none of which assigns the key on that type (AppHeader.tsx builds BreadcrumbItem[] segments without onClick; ObjectCalendar.tsx declares its own local CalendarSchema; chatbot renderer.tsx intersects ChatbotSchema with onSend/onClear, and onSendMessage there is the Chatbot component's own prop). @object-ui/core was type-checked for real against the rebuilt dist; the full turbo run type-check --filter='...@object-ui/types' needs the whole repo built and exceeds the foreground cap on a shared box — CI runs it.

Changeset

.changeset/6124-handler-keys-json-refusal.md@object-ui/types: minor (objectui refuses major); states the accept-set change plainly.

Not in this PR (for the PM)

🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…BY NAME (objectui#6124)
The zod mirrors declared 58 `on*` keys (26 distinct) across eight files as
`z.function()` — a declaration no JSON document can satisfy on a JSON-authored
vocabulary. A JSON author who wrote `onClick: { "action": "toast" }` got zod's
bare `invalid_type … expected function`, naming the key and nothing else.
Ruled shape (maintainer 2026-08-30, batch #8: Q1→A, Q2→A+C, Q3→A, Q4→B):
every site keeps its declaration and becomes a NAMED refusal arm in the #5099
`z.custom` + guidance shape (`handlerKeyRefusal()` in `zod/tombstone.zod.ts`):
the message names the key, says why JSON cannot author it, and points at the
node-type spelling PR #6498 established. The same string is the `.describe()`.
Deletion was measured and refused: under `BaseSchema.passthrough()` an
undeclared key is KEPT, and `onClick` rides `SDUI_DOM_PASS_THROUGH_KEYS` into
the DOM listener slot where React throws at click.
Accept-set change (Clause ②): a live function is refused on the mirror too.
Measured on this tree, the only runtime `safeParse` doors into these mirrors
are the CLI validators and `validateSchema` / `safeValidateSchema`;
`SchemaRenderer` validates through `@object-ui/core`'s structural validator.
TypeScript face, measured per key: `SchemaRenderer` spreads every non-metadata
schema key as a React prop, so 36 keys whose function value reaches a renderer
(read off `schema.*`, called as `props.onX`, or spread onto a Radix root / DOM
listener slot) keep their function type; 22 keys nothing reads carry the
`?: never` tombstone (ADR-0049). The parity ledger records the 35 registered
runtime-slot keys as deliberate drift (28 pairs; `MenuItemSchema` is a lazy
union outside the registry).
Out of scope per the ruling: `cell` / `custom` / `validate` /
`renderCellEditor` stay `z.function()`; `EventHandlersSchema` is objectui#6910.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRRumy89BYdW9ogbcdHTho
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.3 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-CljwiCD4.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.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.72KB2.24KB
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

@os-litant
os-litant marked this pull request as ready for review September 2, 2026 08:49
@os-litant
os-litant added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit a8d5c71Sep 2, 2026
32 checks passed
@os-litant
os-litant deleted the claude/issue-6124-on-handler-keys-refusal-arm branch September 2, 2026 09:04
os-litant added a commit that referenced this pull request Sep 2, 2026
…rable props
objectui#6124 (PR #7339) split the `on*` handler keys in two: 36 runtime
slots keep their function type, and 22 keys nothing reads became `?: never`
tombstones with named refusal arms on the zod mirror. Ten rows across eight
`content/docs` pages went on listing some of the retired 22 as callable
props — a key that is now a `tsc` error to assign and a refusal by name at
authoring time.
The card named three pages (`api/schema-reference.md`,
`data-display/tree-view.mdx`, `form/input-otp.mdx`, five rows); a whole-docs
census resolving each row's `(interface, key)` pair against the shipped
declaration found five more rows on five more pages —
`basic/button-group.mdx` (`ButtonGroupButton.onClick`), and `.onChange` on
`form/calendar.mdx`, `form/combobox.mdx`, `form/command.mdx`,
`form/radio-group.mdx`. A key NAME cannot decide this: `onChange` is retired
on 8 schemas and live on 14, and `input-otp.mdx` carries both dispositions
one line apart.
Seven pages have the retired rows REMOVED. `basic/button-group.mdx` is the
one "marked retired" page and not by taste: `button-group-doc-surface-6347`
asserts set equality between that page's `ButtonGroupButton` block and the
mirror's `.shape`, and a refusal arm is still a key of that shape, so the row
must stay — spelled `never`, with the node-type pointer the tombstone JSDoc
uses. That sibling pin's `onClick` case is updated to read the mirror's
refusal instead of restating the now-stale `() => void`.
The new pin measures the retired population off `packages/types/src` rather
than hand-listing it, flags rows (not prose), and carries six runtime-slot
rows as the blanket-sweep control — two of them on pages this change edits.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRRumy89BYdW9ogbcdHTho
os-project-manager pushed a commit that referenced this pull request Sep 3, 2026
…fuse BY NAME
The objectui#6182 ruling (2026-08-25, option A): the handler-expression string
dialect is not a supported authoring form, on either face. Executed in the
objectui#6124 shape (PR #7339) over the eight sites that PR's `z.function(`
census could not see: four `z.string()` mirrors, three `z.any()` mirrors and
one multi-line `z.function()`.
Per key, measured on origin/main d88e20f:
runtime slot (callable kept, mirror refuses by name):
views.zod.ts DetailViewSchema.onBack — detail-view spreads onto DetailView,
handleBack CALLS onBack(); TS twin
declared `string`, now `() => void`
crud.zod.ts DetailSchema.onBack — register('detail', DetailView)
crud.zod.ts ActionSchema.onClick — ActionRunner awaits action.onClick()
complex.zod.ts CalendarViewSchema.onEventClick — pickHostCallbacks
retired (`?: never`, mirror refuses by name):
app.zod.ts AppActionSchema.onClick — nothing reads AppComponentSchema.actions[]
reports.zod.ts ReportBuilderSchema.onSave / onCancel — no report-builder renderer
crud.zod.ts CRUDDialogSchema.onClose — no crud-dialog renderer
No key was read as a string and dispatched (the card's STOP class did not occur).
The three views.zod.ts event-NAME keys (PR #6899) are untouched; the new pin reads
their describe text as the reason they survive the census.
zod-mirror-parity.test.ts gains the three runtime-slot drift rows (39 entries /
56 keys, counted); component-docs-retired-handler-keys-7340.test.ts records the
ruled move of the `?: never` census (22 → 26) and the two names now retired
everywhere (`onClose`, `onSave`); phase2-schemas.test.ts keeps its old
string-callback fixture as the negative reading. content/docs/core/app-schema.mdx
carries the two edits the #7340 pin and check:doc-snippets require.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-litant@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(types): the 58 on* handler keys declared z.function() refuse BY NAME (objectui#6124) - #7339

Merged
os-litant merged 1 commit into
mainfrom
claude/issue-6124-on-handler-keys-refusal-arm
Sep 2, 2026
Merged

fix(types): the 58 on* handler keys declared z.function() refuse BY NAME (objectui#6124)#7339
os-litant merged 1 commit into
mainfrom
claude/issue-6124-on-handler-keys-refusal-arm

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes#6124

What this PR does

The @object-ui/types zod mirrors declared 58 on* handler keys (26 distinct) across 8 files as z.function() — a declaration no JSON document can satisfy on a JSON-authored vocabulary. A JSON author who wrote onClick: { "action": "toast" } was refused with zod's bare invalid_type … expected function, received object, which names the key and nothing else.

This PR executes the maintainer ruling of 2026-08-30 (director seat, batch #8, verbatim 「批次 #8 同意」, comment 5469346437 on #6124): Q1 → A, Q2 → A with C, Q3 → A, Q4 → B.

Clause ②: yes — the published validators' accept set moves (a function value that parsed green is now refused). Contract-review tier: this PR stays draft and carries needs:contract-review.

Census on the base (c93b4d5f3, re-run, not copied)

Anchored ^\s*on[A-Z][A-Za-z]*: z\.function\( over packages/types/src/zod/*.ts: 58 sites, 26 distinct keys, 8 files (complex 10, data-display 8, disclosure 3, feedback 1, form 20, layout 2, navigation 4, overlay 10). All key: z.function( sites: 62 = 58 + the four non-on* (cell, custom, renderCellEditor, validate). Raw z.function( occurrences: 64 = 62 + EventHandlersSchema + one mention inside a describe string. The card's 28/60 and the ruling's 29/61 are both stale; the close condition is this table.

Schema (file)KeyTS twinRuntime consumer measuredDisposition
KanbanSchema (complex)onCardMove, onCardClickcomplex.tsplugin-kanban/index.tsx:193-194 forwards schema.onCardMove / schema.onCardClickruntime slot
KanbanSchema (complex)onColumnAdd, onCardAddcomplex.tsrenderer takes ({ schema }), never reads themretired → ?: never
CalendarViewSchema (complex)onViewChangecomplex.tscalendar-view-renderer.tsx:319pickHostCallbacks(rest) (function values only) → CalendarViewruntime slot
FilterBuilderSchema (complex)onChangecomplex.tsfilter-builder.tsx:26 calls props.onChangeruntime slot
CarouselSchema (complex)onSlideChangecomplex.ts{...props} onto a div; CarouselProps has no such propretired
ChatbotSchema (complex)onError, onSendcomplex.tsplugin-chatbot/renderer.tsx:89,94useObjectChatruntime slot
ChatbotSchema (complex)onSendMessagecomplex.tsrenderer wires its own handleSendMessage; toDomProps drops the keyretired
AlertSchema (data-display)onDismissdata-display.ts{...props} spread onto the Alert element (no such event)retired
ListItemSchema (data-display)onClickListItemlist.tsx renders each item as a plain li with its contentretired
DataTableSchema (data-display)onRowEdit, onRowDelete, onSelectionChange, onColumnsReorderdata-display.tsdata-table.tsx:551,576,1341,1494 call them off schema.*runtime slot
TreeViewSchema (data-display)onSelectChange, onExpandChangedata-display.ts{...props} onto a div; nodes get only onNodeClickretired
AccordionSchema / CollapsibleSchema / ToggleGroupSchema (disclosure)onValueChange / onOpenChange / onValueChangedisclosure.tsleftover props spread onto the Radix root, where the prop is realruntime slot
ToastSchema (feedback)onDismissfeedback.tstoast.tsx takes ({ schema }), never reads itretired
ButtonSchema (form)onClickform.tsbutton.tsx:67toFormControlDomProps forwards it (onClickSDUI_DOM_PASS_THROUGH_KEYS)runtime slot
InputSchema, TextareaSchema, SelectSchema, CheckboxSchema, FileUploadSchema, DatePickerSchema, InputOTPSchema (form)onChangeform.tseach renderer destructures onChange from props and calls itruntime slot
RadioGroupSchema, SwitchSchema, ToggleSchema, SliderSchema, ComboboxSchema (form)onChangeform.tsonly toFormControlDomProps's whitelist is forwarded; no by-name readretired
CalendarSchema (form)onChangeform.ts{...props} onto DayPicker, whose callback is onSelectretired
CommandSchema (form)onChangeform.tslands on cmdk's root div as a DOM change listener (SyntheticEvent) — not the declared (value: string) => voidretired (leak noted)
InputOTPSchema (form)onCompleteform.tswhitelist drops itretired
FormSchema (form)onSubmit, onChange, onCancelform.tsform.tsx:1002-1005 destructures off schema; called at :2008, :1869, :2096runtime slot
CodeEditorSchema (form)onChangeform.tsplugin-editor/index.tsx:48onChange ?? schema.onChangeruntime slot
CardSchema (layout)onClicklayout.tscard.tsx:35,46 reads props.onClick, spreads onto the Card elementruntime slot
TabsSchema (layout)onValueChangelayout.tstabs.tsx:45{...tabsProps} after its own onValueChangeruntime slot
BreadcrumbItemSchema (navigation)onClickBreadcrumbItembreadcrumb.tsx renders links; never reads itretired
SidebarSchema (navigation)onCollapsedChangenavigation.ts{...props} spread onto the Sidebar component; no such propretired
PaginationSchema (navigation)onPageChangenavigation.tspagination.tsx:32 calls props.onPageChangeruntime slot
ButtonGroupButtonSchema (navigation)onClickButtonGroupButtonbutton-group.tsx renders each Button without a click handlerretired
DialogSchema, SheetSchema, DrawerSchema, PopoverSchema, HoverCardSchema, DropdownMenuSchema, AlertDialogSchema (overlay)onOpenChangeoverlay.ts{...props} onto the Radix / vaul root, where onOpenChange is realruntime slot
AlertDialogSchema (overlay)onConfirm, onCanceloverlay.tsfooter wired to schema.onAction and AlertDialogCancel; never readretired
MenuItemSchema (overlay)onClickMenuCommandItemdropdown-menu.tsx:77, context-menu.tsx:75, menubar.tsx:53,67 call item.onClick?.()runtime slot

Totals: 36 runtime slots + 22 retired = 58.

Parity ledger (zod-mirror-parity.test.ts) — every changed row

The mirror's z.input for a refusal arm is undefined, so a runtime-slot key (TS keeps the function) is a measured TYPE drift and lands in KnownDrift; a retired key (?: never) meets undefined on both sides and does not drift. 28 pairs, 35 keys (the 36th, MenuItemSchema.onClick, is a lazy union in EXCLUSIONS). Header counts re-read: 12 → 36 entries, 17 → 52 keys, 146 → 122 pairs with no entry; a class note above ButtonSchema explains the deliberate two-face divergence.

Extended (4): complex.zod.ts#ChatbotSchema: 'body' | 'onError' | 'onSend' · complex.zod.ts#FilterBuilderSchema: 'fields' | 'onChange' · data-display.zod.ts#DataTableSchema: 'rowActions' | 'onRowEdit' | 'onRowDelete' | 'onSelectionChange' | 'onColumnsReorder' · form.zod.ts#FormSchema: 'fields' | 'mode' | 'onSubmit' | 'onChange' | 'onCancel'.

New (24): complex.zod.ts#CalendarViewSchema: 'onViewChange' · complex.zod.ts#KanbanSchema: 'onCardMove' | 'onCardClick' · disclosure.zod.ts#AccordionSchema: 'onValueChange' · disclosure.zod.ts#CollapsibleSchema: 'onOpenChange' · disclosure.zod.ts#ToggleGroupSchema: 'onValueChange' · form.zod.ts#ButtonSchema: 'onClick' · form.zod.ts#CheckboxSchema: 'onChange' · form.zod.ts#CodeEditorSchema: 'onChange' · form.zod.ts#DatePickerSchema: 'onChange' · form.zod.ts#FileUploadSchema: 'onChange' · form.zod.ts#InputOTPSchema: 'onChange' · form.zod.ts#InputSchema: 'onChange' · form.zod.ts#SelectSchema: 'onChange' · form.zod.ts#TextareaSchema: 'onChange' · layout.zod.ts#CardSchema: 'onClick' · layout.zod.ts#TabsSchema: 'onValueChange' · navigation.zod.ts#PaginationSchema: 'onPageChange' · overlay.zod.ts#AlertDialogSchema: 'onOpenChange' · overlay.zod.ts#DialogSchema: 'onOpenChange' · overlay.zod.ts#DrawerSchema: 'onOpenChange' · overlay.zod.ts#DropdownMenuSchema: 'onOpenChange' · overlay.zod.ts#HoverCardSchema: 'onOpenChange' · overlay.zod.ts#PopoverSchema: 'onOpenChange' · overlay.zod.ts#SheetSchema: 'onOpenChange'.

Tests

New pin packages/types/src/__tests__/handler-keys-json-refusal-6124.test.ts (240 cases): anchored source census (0 on* sites; the 4 non-on* sites pinned by name), 58 × {declared on the shape with the #6124 guidance · authored object refused with code: 'custom', path [key], message naming the key, "type" and action:button, message === describe · live function refused · isolated shape green without the key}, wording split (RUNTIME SLOT vs RETIRED), the passthrough counter-probe (omit → green with the object kept), arm ≠ tombstone (custom vs invalid_type), and type-level pins: 22 × RetiredIsNever, 36 × KeepsFunction, both helpers shown able to fail. Compiled by tsconfig.test.jsontsc --listFiles lists the file (544 files).

Three existing tests restated for the accept-set change (verdicts unchanged, messages moved): component-fixture-declared-keys.test.ts counter-probe (still RED, now custom + named guidance), menu-item-union.test.ts (function parses on the TS face only; union arm errors dug), chatbot-authoring-face-keys.test.ts (onSend refused by name on the mirror).

Red-first on the unmodified tree (c93b4d5f3, pin file only): vitest Tests 178 failed | 62 passed (240) — census found 58 sites; object refused with invalid_type not custom; functions parsed GREEN; tsc -p tsconfig.test.json: 22 × TS2344 on RetiredIsNever exactly as predicted.

Union on the final commit 11fd53500 (all through os-verify-lock.sh, verdict lines quoted): pnpm --filter @object-ui/types run type-check (echoed tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json) type-check-exit=0 · pnpm exec vitest run packages/types/ examples/schema-catalog/test/component-fixture-declared-keys.test.ts --maxWorkers=2Test Files 88 passed (88) / Tests 1455 passed (1455) · pnpm --filter @object-ui/types build✓ dist completeness: 1 package(s) complete (118 emitted files verified) · downstream pnpm --filter @object-ui/core run type-check against the rebuilt dist (core's only workspace dep is @object-ui/types) → exit 0.

Ablation (after the commit; trap … EXIT INT TERM; absolute paths): ButtonSchema.onClick reverted to z.function().optional().describe('Click handler') — on-disk proof arm-before=1 mut-before=0 → arm-after=0 mut-after=1, git diff --stat 1/1; no build needed (the pin imports ../zod/form.zod from SOURCE). Result: Tests 6 failed | 234 passed (240) — census (1 site), Button declared/describe, Button object (invalid_type), Button function (GREEN), wording, arm-vs-tombstone — exactly the Button rows. Restore: git checkout HEAD -- ABS_PATH, git diff HEAD empty, git hash-object = git rev-parse HEAD:… = 23a63abe…, git status clean.

Gates (unlocked check:*, verdict lines): check-changeset-presence✅ 21 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s) · check-changeset-no-major✅ No changeset declares a major bump · check:control-bytes✅ OK (scanned 6023 tracked text file(s)) · check:spec-symbols✅ spec symbol derivation: 1333 files scanned · check:doc-types✅ Every documented component type is registered · check:vi-mock-inherit / check:vi-mock-specifiers✅ OK.

Lint, narrowed and measured:eslint --no-inline-config --format json over the 22 staged .ts files → population from eslint's own json output = 22 files, 0 errors, 54 warnings; every warning is @typescript-eslint/no-explicit-any and each warned file's merge-base blob lints to the identical E/W count (complex.ts 0/8, data-display.ts 0/30, form.ts 0/10, complex.zod.ts 0/2, data-display.zod.ts 0/1, navigation.zod.ts 0/2, overlay.zod.ts 0/1) — pre-existing; config invariance: eslint.config.js has no projectService / parserOptions.project, so this diff cannot move an untouched file's verdict. Repo-wide pnpm lint left to CI.

Downstream type-check, narrowed and declared: the only breaking TS change is the 22 ?: never members. A read census over packages/*/src, apps/*/src, examples, scripts for files naming both an affected interface and its retired key found three, none of which assigns the key on that type (AppHeader.tsx builds BreadcrumbItem[] segments without onClick; ObjectCalendar.tsx declares its own local CalendarSchema; chatbot renderer.tsx intersects ChatbotSchema with onSend/onClear, and onSendMessage there is the Chatbot component's own prop). @object-ui/core was type-checked for real against the rebuilt dist; the full turbo run type-check --filter='...@object-ui/types' needs the whole repo built and exceeds the foreground cap on a shared box — CI runs it.

Changeset

.changeset/6124-handler-keys-json-refusal.md@object-ui/types: minor (objectui refuses major); states the accept-set change plainly.

Not in this PR (for the PM)

🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…BY NAME (objectui#6124)
The zod mirrors declared 58 `on*` keys (26 distinct) across eight files as
`z.function()` — a declaration no JSON document can satisfy on a JSON-authored
vocabulary. A JSON author who wrote `onClick: { "action": "toast" }` got zod's
bare `invalid_type … expected function`, naming the key and nothing else.
Ruled shape (maintainer 2026-08-30, batch #8: Q1→A, Q2→A+C, Q3→A, Q4→B):
every site keeps its declaration and becomes a NAMED refusal arm in the #5099
`z.custom` + guidance shape (`handlerKeyRefusal()` in `zod/tombstone.zod.ts`):
the message names the key, says why JSON cannot author it, and points at the
node-type spelling PR #6498 established. The same string is the `.describe()`.
Deletion was measured and refused: under `BaseSchema.passthrough()` an
undeclared key is KEPT, and `onClick` rides `SDUI_DOM_PASS_THROUGH_KEYS` into
the DOM listener slot where React throws at click.
Accept-set change (Clause ②): a live function is refused on the mirror too.
Measured on this tree, the only runtime `safeParse` doors into these mirrors
are the CLI validators and `validateSchema` / `safeValidateSchema`;
`SchemaRenderer` validates through `@object-ui/core`'s structural validator.
TypeScript face, measured per key: `SchemaRenderer` spreads every non-metadata
schema key as a React prop, so 36 keys whose function value reaches a renderer
(read off `schema.*`, called as `props.onX`, or spread onto a Radix root / DOM
listener slot) keep their function type; 22 keys nothing reads carry the
`?: never` tombstone (ADR-0049). The parity ledger records the 35 registered
runtime-slot keys as deliberate drift (28 pairs; `MenuItemSchema` is a lazy
union outside the registry).
Out of scope per the ruling: `cell` / `custom` / `validate` /
`renderCellEditor` stay `z.function()`; `EventHandlersSchema` is objectui#6910.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRRumy89BYdW9ogbcdHTho
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.3 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-CljwiCD4.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.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.72KB2.24KB
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

@os-litant
os-litant marked this pull request as ready for review September 2, 2026 08:49
@os-litant
os-litant added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit a8d5c71Sep 2, 2026
32 checks passed
@os-litant
os-litant deleted the claude/issue-6124-on-handler-keys-refusal-arm branch September 2, 2026 09:04
os-litant added a commit that referenced this pull request Sep 2, 2026
…rable props
objectui#6124 (PR #7339) split the `on*` handler keys in two: 36 runtime
slots keep their function type, and 22 keys nothing reads became `?: never`
tombstones with named refusal arms on the zod mirror. Ten rows across eight
`content/docs` pages went on listing some of the retired 22 as callable
props — a key that is now a `tsc` error to assign and a refusal by name at
authoring time.
The card named three pages (`api/schema-reference.md`,
`data-display/tree-view.mdx`, `form/input-otp.mdx`, five rows); a whole-docs
census resolving each row's `(interface, key)` pair against the shipped
declaration found five more rows on five more pages —
`basic/button-group.mdx` (`ButtonGroupButton.onClick`), and `.onChange` on
`form/calendar.mdx`, `form/combobox.mdx`, `form/command.mdx`,
`form/radio-group.mdx`. A key NAME cannot decide this: `onChange` is retired
on 8 schemas and live on 14, and `input-otp.mdx` carries both dispositions
one line apart.
Seven pages have the retired rows REMOVED. `basic/button-group.mdx` is the
one "marked retired" page and not by taste: `button-group-doc-surface-6347`
asserts set equality between that page's `ButtonGroupButton` block and the
mirror's `.shape`, and a refusal arm is still a key of that shape, so the row
must stay — spelled `never`, with the node-type pointer the tombstone JSDoc
uses. That sibling pin's `onClick` case is updated to read the mirror's
refusal instead of restating the now-stale `() => void`.
The new pin measures the retired population off `packages/types/src` rather
than hand-listing it, flags rows (not prose), and carries six runtime-slot
rows as the blanket-sweep control — two of them on pages this change edits.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRRumy89BYdW9ogbcdHTho
os-project-manager pushed a commit that referenced this pull request Sep 3, 2026
…fuse BY NAME
The objectui#6182 ruling (2026-08-25, option A): the handler-expression string
dialect is not a supported authoring form, on either face. Executed in the
objectui#6124 shape (PR #7339) over the eight sites that PR's `z.function(`
census could not see: four `z.string()` mirrors, three `z.any()` mirrors and
one multi-line `z.function()`.
Per key, measured on origin/main d88e20f:
runtime slot (callable kept, mirror refuses by name):
views.zod.ts DetailViewSchema.onBack — detail-view spreads onto DetailView,
handleBack CALLS onBack(); TS twin
declared `string`, now `() => void`
crud.zod.ts DetailSchema.onBack — register('detail', DetailView)
crud.zod.ts ActionSchema.onClick — ActionRunner awaits action.onClick()
complex.zod.ts CalendarViewSchema.onEventClick — pickHostCallbacks
retired (`?: never`, mirror refuses by name):
app.zod.ts AppActionSchema.onClick — nothing reads AppComponentSchema.actions[]
reports.zod.ts ReportBuilderSchema.onSave / onCancel — no report-builder renderer
crud.zod.ts CRUDDialogSchema.onClose — no crud-dialog renderer
No key was read as a string and dispatched (the card's STOP class did not occur).
The three views.zod.ts event-NAME keys (PR #6899) are untouched; the new pin reads
their describe text as the reason they survive the census.
zod-mirror-parity.test.ts gains the three runtime-slot drift rows (39 entries /
56 keys, counted); component-docs-retired-handler-keys-7340.test.ts records the
ruled move of the `?: never` census (22 → 26) and the two names now retired
everywhere (`onClose`, `onSave`); phase2-schemas.test.ts keeps its old
string-callback fixture as the negative reading. content/docs/core/app-schema.mdx
carries the two edits the #7340 pin and check:doc-snippets require.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-litant@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(types): the 58 on* handler keys declared z.function() refuse BY NAME (objectui#6124) - #7339

Merged
os-litant merged 1 commit into
mainfrom
claude/issue-6124-on-handler-keys-refusal-arm
Sep 2, 2026
Merged

fix(types): the 58 on* handler keys declared z.function() refuse BY NAME (objectui#6124)#7339
os-litant merged 1 commit into
mainfrom
claude/issue-6124-on-handler-keys-refusal-arm

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes#6124

What this PR does

The @object-ui/types zod mirrors declared 58 on* handler keys (26 distinct) across 8 files as z.function() — a declaration no JSON document can satisfy on a JSON-authored vocabulary. A JSON author who wrote onClick: { "action": "toast" } was refused with zod's bare invalid_type … expected function, received object, which names the key and nothing else.

This PR executes the maintainer ruling of 2026-08-30 (director seat, batch #8, verbatim 「批次 #8 同意」, comment 5469346437 on #6124): Q1 → A, Q2 → A with C, Q3 → A, Q4 → B.

Clause ②: yes — the published validators' accept set moves (a function value that parsed green is now refused). Contract-review tier: this PR stays draft and carries needs:contract-review.

Census on the base (c93b4d5f3, re-run, not copied)

Anchored ^\s*on[A-Z][A-Za-z]*: z\.function\( over packages/types/src/zod/*.ts: 58 sites, 26 distinct keys, 8 files (complex 10, data-display 8, disclosure 3, feedback 1, form 20, layout 2, navigation 4, overlay 10). All key: z.function( sites: 62 = 58 + the four non-on* (cell, custom, renderCellEditor, validate). Raw z.function( occurrences: 64 = 62 + EventHandlersSchema + one mention inside a describe string. The card's 28/60 and the ruling's 29/61 are both stale; the close condition is this table.

Schema (file)KeyTS twinRuntime consumer measuredDisposition
KanbanSchema (complex)onCardMove, onCardClickcomplex.tsplugin-kanban/index.tsx:193-194 forwards schema.onCardMove / schema.onCardClickruntime slot
KanbanSchema (complex)onColumnAdd, onCardAddcomplex.tsrenderer takes ({ schema }), never reads themretired → ?: never
CalendarViewSchema (complex)onViewChangecomplex.tscalendar-view-renderer.tsx:319pickHostCallbacks(rest) (function values only) → CalendarViewruntime slot
FilterBuilderSchema (complex)onChangecomplex.tsfilter-builder.tsx:26 calls props.onChangeruntime slot
CarouselSchema (complex)onSlideChangecomplex.ts{...props} onto a div; CarouselProps has no such propretired
ChatbotSchema (complex)onError, onSendcomplex.tsplugin-chatbot/renderer.tsx:89,94useObjectChatruntime slot
ChatbotSchema (complex)onSendMessagecomplex.tsrenderer wires its own handleSendMessage; toDomProps drops the keyretired
AlertSchema (data-display)onDismissdata-display.ts{...props} spread onto the Alert element (no such event)retired
ListItemSchema (data-display)onClickListItemlist.tsx renders each item as a plain li with its contentretired
DataTableSchema (data-display)onRowEdit, onRowDelete, onSelectionChange, onColumnsReorderdata-display.tsdata-table.tsx:551,576,1341,1494 call them off schema.*runtime slot
TreeViewSchema (data-display)onSelectChange, onExpandChangedata-display.ts{...props} onto a div; nodes get only onNodeClickretired
AccordionSchema / CollapsibleSchema / ToggleGroupSchema (disclosure)onValueChange / onOpenChange / onValueChangedisclosure.tsleftover props spread onto the Radix root, where the prop is realruntime slot
ToastSchema (feedback)onDismissfeedback.tstoast.tsx takes ({ schema }), never reads itretired
ButtonSchema (form)onClickform.tsbutton.tsx:67toFormControlDomProps forwards it (onClickSDUI_DOM_PASS_THROUGH_KEYS)runtime slot
InputSchema, TextareaSchema, SelectSchema, CheckboxSchema, FileUploadSchema, DatePickerSchema, InputOTPSchema (form)onChangeform.tseach renderer destructures onChange from props and calls itruntime slot
RadioGroupSchema, SwitchSchema, ToggleSchema, SliderSchema, ComboboxSchema (form)onChangeform.tsonly toFormControlDomProps's whitelist is forwarded; no by-name readretired
CalendarSchema (form)onChangeform.ts{...props} onto DayPicker, whose callback is onSelectretired
CommandSchema (form)onChangeform.tslands on cmdk's root div as a DOM change listener (SyntheticEvent) — not the declared (value: string) => voidretired (leak noted)
InputOTPSchema (form)onCompleteform.tswhitelist drops itretired
FormSchema (form)onSubmit, onChange, onCancelform.tsform.tsx:1002-1005 destructures off schema; called at :2008, :1869, :2096runtime slot
CodeEditorSchema (form)onChangeform.tsplugin-editor/index.tsx:48onChange ?? schema.onChangeruntime slot
CardSchema (layout)onClicklayout.tscard.tsx:35,46 reads props.onClick, spreads onto the Card elementruntime slot
TabsSchema (layout)onValueChangelayout.tstabs.tsx:45{...tabsProps} after its own onValueChangeruntime slot
BreadcrumbItemSchema (navigation)onClickBreadcrumbItembreadcrumb.tsx renders links; never reads itretired
SidebarSchema (navigation)onCollapsedChangenavigation.ts{...props} spread onto the Sidebar component; no such propretired
PaginationSchema (navigation)onPageChangenavigation.tspagination.tsx:32 calls props.onPageChangeruntime slot
ButtonGroupButtonSchema (navigation)onClickButtonGroupButtonbutton-group.tsx renders each Button without a click handlerretired
DialogSchema, SheetSchema, DrawerSchema, PopoverSchema, HoverCardSchema, DropdownMenuSchema, AlertDialogSchema (overlay)onOpenChangeoverlay.ts{...props} onto the Radix / vaul root, where onOpenChange is realruntime slot
AlertDialogSchema (overlay)onConfirm, onCanceloverlay.tsfooter wired to schema.onAction and AlertDialogCancel; never readretired
MenuItemSchema (overlay)onClickMenuCommandItemdropdown-menu.tsx:77, context-menu.tsx:75, menubar.tsx:53,67 call item.onClick?.()runtime slot

Totals: 36 runtime slots + 22 retired = 58.

Parity ledger (zod-mirror-parity.test.ts) — every changed row

The mirror's z.input for a refusal arm is undefined, so a runtime-slot key (TS keeps the function) is a measured TYPE drift and lands in KnownDrift; a retired key (?: never) meets undefined on both sides and does not drift. 28 pairs, 35 keys (the 36th, MenuItemSchema.onClick, is a lazy union in EXCLUSIONS). Header counts re-read: 12 → 36 entries, 17 → 52 keys, 146 → 122 pairs with no entry; a class note above ButtonSchema explains the deliberate two-face divergence.

Extended (4): complex.zod.ts#ChatbotSchema: 'body' | 'onError' | 'onSend' · complex.zod.ts#FilterBuilderSchema: 'fields' | 'onChange' · data-display.zod.ts#DataTableSchema: 'rowActions' | 'onRowEdit' | 'onRowDelete' | 'onSelectionChange' | 'onColumnsReorder' · form.zod.ts#FormSchema: 'fields' | 'mode' | 'onSubmit' | 'onChange' | 'onCancel'.

New (24): complex.zod.ts#CalendarViewSchema: 'onViewChange' · complex.zod.ts#KanbanSchema: 'onCardMove' | 'onCardClick' · disclosure.zod.ts#AccordionSchema: 'onValueChange' · disclosure.zod.ts#CollapsibleSchema: 'onOpenChange' · disclosure.zod.ts#ToggleGroupSchema: 'onValueChange' · form.zod.ts#ButtonSchema: 'onClick' · form.zod.ts#CheckboxSchema: 'onChange' · form.zod.ts#CodeEditorSchema: 'onChange' · form.zod.ts#DatePickerSchema: 'onChange' · form.zod.ts#FileUploadSchema: 'onChange' · form.zod.ts#InputOTPSchema: 'onChange' · form.zod.ts#InputSchema: 'onChange' · form.zod.ts#SelectSchema: 'onChange' · form.zod.ts#TextareaSchema: 'onChange' · layout.zod.ts#CardSchema: 'onClick' · layout.zod.ts#TabsSchema: 'onValueChange' · navigation.zod.ts#PaginationSchema: 'onPageChange' · overlay.zod.ts#AlertDialogSchema: 'onOpenChange' · overlay.zod.ts#DialogSchema: 'onOpenChange' · overlay.zod.ts#DrawerSchema: 'onOpenChange' · overlay.zod.ts#DropdownMenuSchema: 'onOpenChange' · overlay.zod.ts#HoverCardSchema: 'onOpenChange' · overlay.zod.ts#PopoverSchema: 'onOpenChange' · overlay.zod.ts#SheetSchema: 'onOpenChange'.

Tests

New pin packages/types/src/__tests__/handler-keys-json-refusal-6124.test.ts (240 cases): anchored source census (0 on* sites; the 4 non-on* sites pinned by name), 58 × {declared on the shape with the #6124 guidance · authored object refused with code: 'custom', path [key], message naming the key, "type" and action:button, message === describe · live function refused · isolated shape green without the key}, wording split (RUNTIME SLOT vs RETIRED), the passthrough counter-probe (omit → green with the object kept), arm ≠ tombstone (custom vs invalid_type), and type-level pins: 22 × RetiredIsNever, 36 × KeepsFunction, both helpers shown able to fail. Compiled by tsconfig.test.jsontsc --listFiles lists the file (544 files).

Three existing tests restated for the accept-set change (verdicts unchanged, messages moved): component-fixture-declared-keys.test.ts counter-probe (still RED, now custom + named guidance), menu-item-union.test.ts (function parses on the TS face only; union arm errors dug), chatbot-authoring-face-keys.test.ts (onSend refused by name on the mirror).

Red-first on the unmodified tree (c93b4d5f3, pin file only): vitest Tests 178 failed | 62 passed (240) — census found 58 sites; object refused with invalid_type not custom; functions parsed GREEN; tsc -p tsconfig.test.json: 22 × TS2344 on RetiredIsNever exactly as predicted.

Union on the final commit 11fd53500 (all through os-verify-lock.sh, verdict lines quoted): pnpm --filter @object-ui/types run type-check (echoed tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json) type-check-exit=0 · pnpm exec vitest run packages/types/ examples/schema-catalog/test/component-fixture-declared-keys.test.ts --maxWorkers=2Test Files 88 passed (88) / Tests 1455 passed (1455) · pnpm --filter @object-ui/types build✓ dist completeness: 1 package(s) complete (118 emitted files verified) · downstream pnpm --filter @object-ui/core run type-check against the rebuilt dist (core's only workspace dep is @object-ui/types) → exit 0.

Ablation (after the commit; trap … EXIT INT TERM; absolute paths): ButtonSchema.onClick reverted to z.function().optional().describe('Click handler') — on-disk proof arm-before=1 mut-before=0 → arm-after=0 mut-after=1, git diff --stat 1/1; no build needed (the pin imports ../zod/form.zod from SOURCE). Result: Tests 6 failed | 234 passed (240) — census (1 site), Button declared/describe, Button object (invalid_type), Button function (GREEN), wording, arm-vs-tombstone — exactly the Button rows. Restore: git checkout HEAD -- ABS_PATH, git diff HEAD empty, git hash-object = git rev-parse HEAD:… = 23a63abe…, git status clean.

Gates (unlocked check:*, verdict lines): check-changeset-presence✅ 21 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s) · check-changeset-no-major✅ No changeset declares a major bump · check:control-bytes✅ OK (scanned 6023 tracked text file(s)) · check:spec-symbols✅ spec symbol derivation: 1333 files scanned · check:doc-types✅ Every documented component type is registered · check:vi-mock-inherit / check:vi-mock-specifiers✅ OK.

Lint, narrowed and measured:eslint --no-inline-config --format json over the 22 staged .ts files → population from eslint's own json output = 22 files, 0 errors, 54 warnings; every warning is @typescript-eslint/no-explicit-any and each warned file's merge-base blob lints to the identical E/W count (complex.ts 0/8, data-display.ts 0/30, form.ts 0/10, complex.zod.ts 0/2, data-display.zod.ts 0/1, navigation.zod.ts 0/2, overlay.zod.ts 0/1) — pre-existing; config invariance: eslint.config.js has no projectService / parserOptions.project, so this diff cannot move an untouched file's verdict. Repo-wide pnpm lint left to CI.

Downstream type-check, narrowed and declared: the only breaking TS change is the 22 ?: never members. A read census over packages/*/src, apps/*/src, examples, scripts for files naming both an affected interface and its retired key found three, none of which assigns the key on that type (AppHeader.tsx builds BreadcrumbItem[] segments without onClick; ObjectCalendar.tsx declares its own local CalendarSchema; chatbot renderer.tsx intersects ChatbotSchema with onSend/onClear, and onSendMessage there is the Chatbot component's own prop). @object-ui/core was type-checked for real against the rebuilt dist; the full turbo run type-check --filter='...@object-ui/types' needs the whole repo built and exceeds the foreground cap on a shared box — CI runs it.

Changeset

.changeset/6124-handler-keys-json-refusal.md@object-ui/types: minor (objectui refuses major); states the accept-set change plainly.

Not in this PR (for the PM)

🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…BY NAME (objectui#6124)
The zod mirrors declared 58 `on*` keys (26 distinct) across eight files as
`z.function()` — a declaration no JSON document can satisfy on a JSON-authored
vocabulary. A JSON author who wrote `onClick: { "action": "toast" }` got zod's
bare `invalid_type … expected function`, naming the key and nothing else.
Ruled shape (maintainer 2026-08-30, batch #8: Q1→A, Q2→A+C, Q3→A, Q4→B):
every site keeps its declaration and becomes a NAMED refusal arm in the #5099
`z.custom` + guidance shape (`handlerKeyRefusal()` in `zod/tombstone.zod.ts`):
the message names the key, says why JSON cannot author it, and points at the
node-type spelling PR #6498 established. The same string is the `.describe()`.
Deletion was measured and refused: under `BaseSchema.passthrough()` an
undeclared key is KEPT, and `onClick` rides `SDUI_DOM_PASS_THROUGH_KEYS` into
the DOM listener slot where React throws at click.
Accept-set change (Clause ②): a live function is refused on the mirror too.
Measured on this tree, the only runtime `safeParse` doors into these mirrors
are the CLI validators and `validateSchema` / `safeValidateSchema`;
`SchemaRenderer` validates through `@object-ui/core`'s structural validator.
TypeScript face, measured per key: `SchemaRenderer` spreads every non-metadata
schema key as a React prop, so 36 keys whose function value reaches a renderer
(read off `schema.*`, called as `props.onX`, or spread onto a Radix root / DOM
listener slot) keep their function type; 22 keys nothing reads carry the
`?: never` tombstone (ADR-0049). The parity ledger records the 35 registered
runtime-slot keys as deliberate drift (28 pairs; `MenuItemSchema` is a lazy
union outside the registry).
Out of scope per the ruling: `cell` / `custom` / `validate` /
`renderCellEditor` stay `z.function()`; `EventHandlersSchema` is objectui#6910.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRRumy89BYdW9ogbcdHTho
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.3 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-CljwiCD4.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.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.72KB2.24KB
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

@os-litant
os-litant marked this pull request as ready for review September 2, 2026 08:49
@os-litant
os-litant added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit a8d5c71Sep 2, 2026
32 checks passed
@os-litant
os-litant deleted the claude/issue-6124-on-handler-keys-refusal-arm branch September 2, 2026 09:04
os-litant added a commit that referenced this pull request Sep 2, 2026
…rable props
objectui#6124 (PR #7339) split the `on*` handler keys in two: 36 runtime
slots keep their function type, and 22 keys nothing reads became `?: never`
tombstones with named refusal arms on the zod mirror. Ten rows across eight
`content/docs` pages went on listing some of the retired 22 as callable
props — a key that is now a `tsc` error to assign and a refusal by name at
authoring time.
The card named three pages (`api/schema-reference.md`,
`data-display/tree-view.mdx`, `form/input-otp.mdx`, five rows); a whole-docs
census resolving each row's `(interface, key)` pair against the shipped
declaration found five more rows on five more pages —
`basic/button-group.mdx` (`ButtonGroupButton.onClick`), and `.onChange` on
`form/calendar.mdx`, `form/combobox.mdx`, `form/command.mdx`,
`form/radio-group.mdx`. A key NAME cannot decide this: `onChange` is retired
on 8 schemas and live on 14, and `input-otp.mdx` carries both dispositions
one line apart.
Seven pages have the retired rows REMOVED. `basic/button-group.mdx` is the
one "marked retired" page and not by taste: `button-group-doc-surface-6347`
asserts set equality between that page's `ButtonGroupButton` block and the
mirror's `.shape`, and a refusal arm is still a key of that shape, so the row
must stay — spelled `never`, with the node-type pointer the tombstone JSDoc
uses. That sibling pin's `onClick` case is updated to read the mirror's
refusal instead of restating the now-stale `() => void`.
The new pin measures the retired population off `packages/types/src` rather
than hand-listing it, flags rows (not prose), and carries six runtime-slot
rows as the blanket-sweep control — two of them on pages this change edits.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRRumy89BYdW9ogbcdHTho
os-project-manager pushed a commit that referenced this pull request Sep 3, 2026
…fuse BY NAME
The objectui#6182 ruling (2026-08-25, option A): the handler-expression string
dialect is not a supported authoring form, on either face. Executed in the
objectui#6124 shape (PR #7339) over the eight sites that PR's `z.function(`
census could not see: four `z.string()` mirrors, three `z.any()` mirrors and
one multi-line `z.function()`.
Per key, measured on origin/main d88e20f:
runtime slot (callable kept, mirror refuses by name):
views.zod.ts DetailViewSchema.onBack — detail-view spreads onto DetailView,
handleBack CALLS onBack(); TS twin
declared `string`, now `() => void`
crud.zod.ts DetailSchema.onBack — register('detail', DetailView)
crud.zod.ts ActionSchema.onClick — ActionRunner awaits action.onClick()
complex.zod.ts CalendarViewSchema.onEventClick — pickHostCallbacks
retired (`?: never`, mirror refuses by name):
app.zod.ts AppActionSchema.onClick — nothing reads AppComponentSchema.actions[]
reports.zod.ts ReportBuilderSchema.onSave / onCancel — no report-builder renderer
crud.zod.ts CRUDDialogSchema.onClose — no crud-dialog renderer
No key was read as a string and dispatched (the card's STOP class did not occur).
The three views.zod.ts event-NAME keys (PR #6899) are untouched; the new pin reads
their describe text as the reason they survive the census.
zod-mirror-parity.test.ts gains the three runtime-slot drift rows (39 entries /
56 keys, counted); component-docs-retired-handler-keys-7340.test.ts records the
ruled move of the `?: never` census (22 → 26) and the two names now retired
everywhere (`onClose`, `onSave`); phase2-schemas.test.ts keeps its old
string-callback fixture as the negative reading. content/docs/core/app-schema.mdx
carries the two edits the #7340 pin and check:doc-snippets require.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-litant@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(types): the 58 on* handler keys declared z.function() refuse BY NAME (objectui#6124) - #7339

Merged
os-litant merged 1 commit into
mainfrom
claude/issue-6124-on-handler-keys-refusal-arm
Sep 2, 2026
Merged

fix(types): the 58 on* handler keys declared z.function() refuse BY NAME (objectui#6124)#7339
os-litant merged 1 commit into
mainfrom
claude/issue-6124-on-handler-keys-refusal-arm

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes#6124

What this PR does

The @object-ui/types zod mirrors declared 58 on* handler keys (26 distinct) across 8 files as z.function() — a declaration no JSON document can satisfy on a JSON-authored vocabulary. A JSON author who wrote onClick: { "action": "toast" } was refused with zod's bare invalid_type … expected function, received object, which names the key and nothing else.

This PR executes the maintainer ruling of 2026-08-30 (director seat, batch #8, verbatim 「批次 #8 同意」, comment 5469346437 on #6124): Q1 → A, Q2 → A with C, Q3 → A, Q4 → B.

Clause ②: yes — the published validators' accept set moves (a function value that parsed green is now refused). Contract-review tier: this PR stays draft and carries needs:contract-review.

Census on the base (c93b4d5f3, re-run, not copied)

Anchored ^\s*on[A-Z][A-Za-z]*: z\.function\( over packages/types/src/zod/*.ts: 58 sites, 26 distinct keys, 8 files (complex 10, data-display 8, disclosure 3, feedback 1, form 20, layout 2, navigation 4, overlay 10). All key: z.function( sites: 62 = 58 + the four non-on* (cell, custom, renderCellEditor, validate). Raw z.function( occurrences: 64 = 62 + EventHandlersSchema + one mention inside a describe string. The card's 28/60 and the ruling's 29/61 are both stale; the close condition is this table.

Schema (file)KeyTS twinRuntime consumer measuredDisposition
KanbanSchema (complex)onCardMove, onCardClickcomplex.tsplugin-kanban/index.tsx:193-194 forwards schema.onCardMove / schema.onCardClickruntime slot
KanbanSchema (complex)onColumnAdd, onCardAddcomplex.tsrenderer takes ({ schema }), never reads themretired → ?: never
CalendarViewSchema (complex)onViewChangecomplex.tscalendar-view-renderer.tsx:319pickHostCallbacks(rest) (function values only) → CalendarViewruntime slot
FilterBuilderSchema (complex)onChangecomplex.tsfilter-builder.tsx:26 calls props.onChangeruntime slot
CarouselSchema (complex)onSlideChangecomplex.ts{...props} onto a div; CarouselProps has no such propretired
ChatbotSchema (complex)onError, onSendcomplex.tsplugin-chatbot/renderer.tsx:89,94useObjectChatruntime slot
ChatbotSchema (complex)onSendMessagecomplex.tsrenderer wires its own handleSendMessage; toDomProps drops the keyretired
AlertSchema (data-display)onDismissdata-display.ts{...props} spread onto the Alert element (no such event)retired
ListItemSchema (data-display)onClickListItemlist.tsx renders each item as a plain li with its contentretired
DataTableSchema (data-display)onRowEdit, onRowDelete, onSelectionChange, onColumnsReorderdata-display.tsdata-table.tsx:551,576,1341,1494 call them off schema.*runtime slot
TreeViewSchema (data-display)onSelectChange, onExpandChangedata-display.ts{...props} onto a div; nodes get only onNodeClickretired
AccordionSchema / CollapsibleSchema / ToggleGroupSchema (disclosure)onValueChange / onOpenChange / onValueChangedisclosure.tsleftover props spread onto the Radix root, where the prop is realruntime slot
ToastSchema (feedback)onDismissfeedback.tstoast.tsx takes ({ schema }), never reads itretired
ButtonSchema (form)onClickform.tsbutton.tsx:67toFormControlDomProps forwards it (onClickSDUI_DOM_PASS_THROUGH_KEYS)runtime slot
InputSchema, TextareaSchema, SelectSchema, CheckboxSchema, FileUploadSchema, DatePickerSchema, InputOTPSchema (form)onChangeform.tseach renderer destructures onChange from props and calls itruntime slot
RadioGroupSchema, SwitchSchema, ToggleSchema, SliderSchema, ComboboxSchema (form)onChangeform.tsonly toFormControlDomProps's whitelist is forwarded; no by-name readretired
CalendarSchema (form)onChangeform.ts{...props} onto DayPicker, whose callback is onSelectretired
CommandSchema (form)onChangeform.tslands on cmdk's root div as a DOM change listener (SyntheticEvent) — not the declared (value: string) => voidretired (leak noted)
InputOTPSchema (form)onCompleteform.tswhitelist drops itretired
FormSchema (form)onSubmit, onChange, onCancelform.tsform.tsx:1002-1005 destructures off schema; called at :2008, :1869, :2096runtime slot
CodeEditorSchema (form)onChangeform.tsplugin-editor/index.tsx:48onChange ?? schema.onChangeruntime slot
CardSchema (layout)onClicklayout.tscard.tsx:35,46 reads props.onClick, spreads onto the Card elementruntime slot
TabsSchema (layout)onValueChangelayout.tstabs.tsx:45{...tabsProps} after its own onValueChangeruntime slot
BreadcrumbItemSchema (navigation)onClickBreadcrumbItembreadcrumb.tsx renders links; never reads itretired
SidebarSchema (navigation)onCollapsedChangenavigation.ts{...props} spread onto the Sidebar component; no such propretired
PaginationSchema (navigation)onPageChangenavigation.tspagination.tsx:32 calls props.onPageChangeruntime slot
ButtonGroupButtonSchema (navigation)onClickButtonGroupButtonbutton-group.tsx renders each Button without a click handlerretired
DialogSchema, SheetSchema, DrawerSchema, PopoverSchema, HoverCardSchema, DropdownMenuSchema, AlertDialogSchema (overlay)onOpenChangeoverlay.ts{...props} onto the Radix / vaul root, where onOpenChange is realruntime slot
AlertDialogSchema (overlay)onConfirm, onCanceloverlay.tsfooter wired to schema.onAction and AlertDialogCancel; never readretired
MenuItemSchema (overlay)onClickMenuCommandItemdropdown-menu.tsx:77, context-menu.tsx:75, menubar.tsx:53,67 call item.onClick?.()runtime slot

Totals: 36 runtime slots + 22 retired = 58.

Parity ledger (zod-mirror-parity.test.ts) — every changed row

The mirror's z.input for a refusal arm is undefined, so a runtime-slot key (TS keeps the function) is a measured TYPE drift and lands in KnownDrift; a retired key (?: never) meets undefined on both sides and does not drift. 28 pairs, 35 keys (the 36th, MenuItemSchema.onClick, is a lazy union in EXCLUSIONS). Header counts re-read: 12 → 36 entries, 17 → 52 keys, 146 → 122 pairs with no entry; a class note above ButtonSchema explains the deliberate two-face divergence.

Extended (4): complex.zod.ts#ChatbotSchema: 'body' | 'onError' | 'onSend' · complex.zod.ts#FilterBuilderSchema: 'fields' | 'onChange' · data-display.zod.ts#DataTableSchema: 'rowActions' | 'onRowEdit' | 'onRowDelete' | 'onSelectionChange' | 'onColumnsReorder' · form.zod.ts#FormSchema: 'fields' | 'mode' | 'onSubmit' | 'onChange' | 'onCancel'.

New (24): complex.zod.ts#CalendarViewSchema: 'onViewChange' · complex.zod.ts#KanbanSchema: 'onCardMove' | 'onCardClick' · disclosure.zod.ts#AccordionSchema: 'onValueChange' · disclosure.zod.ts#CollapsibleSchema: 'onOpenChange' · disclosure.zod.ts#ToggleGroupSchema: 'onValueChange' · form.zod.ts#ButtonSchema: 'onClick' · form.zod.ts#CheckboxSchema: 'onChange' · form.zod.ts#CodeEditorSchema: 'onChange' · form.zod.ts#DatePickerSchema: 'onChange' · form.zod.ts#FileUploadSchema: 'onChange' · form.zod.ts#InputOTPSchema: 'onChange' · form.zod.ts#InputSchema: 'onChange' · form.zod.ts#SelectSchema: 'onChange' · form.zod.ts#TextareaSchema: 'onChange' · layout.zod.ts#CardSchema: 'onClick' · layout.zod.ts#TabsSchema: 'onValueChange' · navigation.zod.ts#PaginationSchema: 'onPageChange' · overlay.zod.ts#AlertDialogSchema: 'onOpenChange' · overlay.zod.ts#DialogSchema: 'onOpenChange' · overlay.zod.ts#DrawerSchema: 'onOpenChange' · overlay.zod.ts#DropdownMenuSchema: 'onOpenChange' · overlay.zod.ts#HoverCardSchema: 'onOpenChange' · overlay.zod.ts#PopoverSchema: 'onOpenChange' · overlay.zod.ts#SheetSchema: 'onOpenChange'.

Tests

New pin packages/types/src/__tests__/handler-keys-json-refusal-6124.test.ts (240 cases): anchored source census (0 on* sites; the 4 non-on* sites pinned by name), 58 × {declared on the shape with the #6124 guidance · authored object refused with code: 'custom', path [key], message naming the key, "type" and action:button, message === describe · live function refused · isolated shape green without the key}, wording split (RUNTIME SLOT vs RETIRED), the passthrough counter-probe (omit → green with the object kept), arm ≠ tombstone (custom vs invalid_type), and type-level pins: 22 × RetiredIsNever, 36 × KeepsFunction, both helpers shown able to fail. Compiled by tsconfig.test.jsontsc --listFiles lists the file (544 files).

Three existing tests restated for the accept-set change (verdicts unchanged, messages moved): component-fixture-declared-keys.test.ts counter-probe (still RED, now custom + named guidance), menu-item-union.test.ts (function parses on the TS face only; union arm errors dug), chatbot-authoring-face-keys.test.ts (onSend refused by name on the mirror).

Red-first on the unmodified tree (c93b4d5f3, pin file only): vitest Tests 178 failed | 62 passed (240) — census found 58 sites; object refused with invalid_type not custom; functions parsed GREEN; tsc -p tsconfig.test.json: 22 × TS2344 on RetiredIsNever exactly as predicted.

Union on the final commit 11fd53500 (all through os-verify-lock.sh, verdict lines quoted): pnpm --filter @object-ui/types run type-check (echoed tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json) type-check-exit=0 · pnpm exec vitest run packages/types/ examples/schema-catalog/test/component-fixture-declared-keys.test.ts --maxWorkers=2Test Files 88 passed (88) / Tests 1455 passed (1455) · pnpm --filter @object-ui/types build✓ dist completeness: 1 package(s) complete (118 emitted files verified) · downstream pnpm --filter @object-ui/core run type-check against the rebuilt dist (core's only workspace dep is @object-ui/types) → exit 0.

Ablation (after the commit; trap … EXIT INT TERM; absolute paths): ButtonSchema.onClick reverted to z.function().optional().describe('Click handler') — on-disk proof arm-before=1 mut-before=0 → arm-after=0 mut-after=1, git diff --stat 1/1; no build needed (the pin imports ../zod/form.zod from SOURCE). Result: Tests 6 failed | 234 passed (240) — census (1 site), Button declared/describe, Button object (invalid_type), Button function (GREEN), wording, arm-vs-tombstone — exactly the Button rows. Restore: git checkout HEAD -- ABS_PATH, git diff HEAD empty, git hash-object = git rev-parse HEAD:… = 23a63abe…, git status clean.

Gates (unlocked check:*, verdict lines): check-changeset-presence✅ 21 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s) · check-changeset-no-major✅ No changeset declares a major bump · check:control-bytes✅ OK (scanned 6023 tracked text file(s)) · check:spec-symbols✅ spec symbol derivation: 1333 files scanned · check:doc-types✅ Every documented component type is registered · check:vi-mock-inherit / check:vi-mock-specifiers✅ OK.

Lint, narrowed and measured:eslint --no-inline-config --format json over the 22 staged .ts files → population from eslint's own json output = 22 files, 0 errors, 54 warnings; every warning is @typescript-eslint/no-explicit-any and each warned file's merge-base blob lints to the identical E/W count (complex.ts 0/8, data-display.ts 0/30, form.ts 0/10, complex.zod.ts 0/2, data-display.zod.ts 0/1, navigation.zod.ts 0/2, overlay.zod.ts 0/1) — pre-existing; config invariance: eslint.config.js has no projectService / parserOptions.project, so this diff cannot move an untouched file's verdict. Repo-wide pnpm lint left to CI.

Downstream type-check, narrowed and declared: the only breaking TS change is the 22 ?: never members. A read census over packages/*/src, apps/*/src, examples, scripts for files naming both an affected interface and its retired key found three, none of which assigns the key on that type (AppHeader.tsx builds BreadcrumbItem[] segments without onClick; ObjectCalendar.tsx declares its own local CalendarSchema; chatbot renderer.tsx intersects ChatbotSchema with onSend/onClear, and onSendMessage there is the Chatbot component's own prop). @object-ui/core was type-checked for real against the rebuilt dist; the full turbo run type-check --filter='...@object-ui/types' needs the whole repo built and exceeds the foreground cap on a shared box — CI runs it.

Changeset

.changeset/6124-handler-keys-json-refusal.md@object-ui/types: minor (objectui refuses major); states the accept-set change plainly.

Not in this PR (for the PM)

🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…BY NAME (objectui#6124)
The zod mirrors declared 58 `on*` keys (26 distinct) across eight files as
`z.function()` — a declaration no JSON document can satisfy on a JSON-authored
vocabulary. A JSON author who wrote `onClick: { "action": "toast" }` got zod's
bare `invalid_type … expected function`, naming the key and nothing else.
Ruled shape (maintainer 2026-08-30, batch #8: Q1→A, Q2→A+C, Q3→A, Q4→B):
every site keeps its declaration and becomes a NAMED refusal arm in the #5099
`z.custom` + guidance shape (`handlerKeyRefusal()` in `zod/tombstone.zod.ts`):
the message names the key, says why JSON cannot author it, and points at the
node-type spelling PR #6498 established. The same string is the `.describe()`.
Deletion was measured and refused: under `BaseSchema.passthrough()` an
undeclared key is KEPT, and `onClick` rides `SDUI_DOM_PASS_THROUGH_KEYS` into
the DOM listener slot where React throws at click.
Accept-set change (Clause ②): a live function is refused on the mirror too.
Measured on this tree, the only runtime `safeParse` doors into these mirrors
are the CLI validators and `validateSchema` / `safeValidateSchema`;
`SchemaRenderer` validates through `@object-ui/core`'s structural validator.
TypeScript face, measured per key: `SchemaRenderer` spreads every non-metadata
schema key as a React prop, so 36 keys whose function value reaches a renderer
(read off `schema.*`, called as `props.onX`, or spread onto a Radix root / DOM
listener slot) keep their function type; 22 keys nothing reads carry the
`?: never` tombstone (ADR-0049). The parity ledger records the 35 registered
runtime-slot keys as deliberate drift (28 pairs; `MenuItemSchema` is a lazy
union outside the registry).
Out of scope per the ruling: `cell` / `custom` / `validate` /
`renderCellEditor` stay `z.function()`; `EventHandlersSchema` is objectui#6910.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRRumy89BYdW9ogbcdHTho
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.3 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-CljwiCD4.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.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.72KB2.24KB
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

@os-litant
os-litant marked this pull request as ready for review September 2, 2026 08:49
@os-litant
os-litant added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit a8d5c71Sep 2, 2026
32 checks passed
@os-litant
os-litant deleted the claude/issue-6124-on-handler-keys-refusal-arm branch September 2, 2026 09:04
os-litant added a commit that referenced this pull request Sep 2, 2026
…rable props
objectui#6124 (PR #7339) split the `on*` handler keys in two: 36 runtime
slots keep their function type, and 22 keys nothing reads became `?: never`
tombstones with named refusal arms on the zod mirror. Ten rows across eight
`content/docs` pages went on listing some of the retired 22 as callable
props — a key that is now a `tsc` error to assign and a refusal by name at
authoring time.
The card named three pages (`api/schema-reference.md`,
`data-display/tree-view.mdx`, `form/input-otp.mdx`, five rows); a whole-docs
census resolving each row's `(interface, key)` pair against the shipped
declaration found five more rows on five more pages —
`basic/button-group.mdx` (`ButtonGroupButton.onClick`), and `.onChange` on
`form/calendar.mdx`, `form/combobox.mdx`, `form/command.mdx`,
`form/radio-group.mdx`. A key NAME cannot decide this: `onChange` is retired
on 8 schemas and live on 14, and `input-otp.mdx` carries both dispositions
one line apart.
Seven pages have the retired rows REMOVED. `basic/button-group.mdx` is the
one "marked retired" page and not by taste: `button-group-doc-surface-6347`
asserts set equality between that page's `ButtonGroupButton` block and the
mirror's `.shape`, and a refusal arm is still a key of that shape, so the row
must stay — spelled `never`, with the node-type pointer the tombstone JSDoc
uses. That sibling pin's `onClick` case is updated to read the mirror's
refusal instead of restating the now-stale `() => void`.
The new pin measures the retired population off `packages/types/src` rather
than hand-listing it, flags rows (not prose), and carries six runtime-slot
rows as the blanket-sweep control — two of them on pages this change edits.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRRumy89BYdW9ogbcdHTho
os-project-manager pushed a commit that referenced this pull request Sep 3, 2026
…fuse BY NAME
The objectui#6182 ruling (2026-08-25, option A): the handler-expression string
dialect is not a supported authoring form, on either face. Executed in the
objectui#6124 shape (PR #7339) over the eight sites that PR's `z.function(`
census could not see: four `z.string()` mirrors, three `z.any()` mirrors and
one multi-line `z.function()`.
Per key, measured on origin/main d88e20f:
runtime slot (callable kept, mirror refuses by name):
views.zod.ts DetailViewSchema.onBack — detail-view spreads onto DetailView,
handleBack CALLS onBack(); TS twin
declared `string`, now `() => void`
crud.zod.ts DetailSchema.onBack — register('detail', DetailView)
crud.zod.ts ActionSchema.onClick — ActionRunner awaits action.onClick()
complex.zod.ts CalendarViewSchema.onEventClick — pickHostCallbacks
retired (`?: never`, mirror refuses by name):
app.zod.ts AppActionSchema.onClick — nothing reads AppComponentSchema.actions[]
reports.zod.ts ReportBuilderSchema.onSave / onCancel — no report-builder renderer
crud.zod.ts CRUDDialogSchema.onClose — no crud-dialog renderer
No key was read as a string and dispatched (the card's STOP class did not occur).
The three views.zod.ts event-NAME keys (PR #6899) are untouched; the new pin reads
their describe text as the reason they survive the census.
zod-mirror-parity.test.ts gains the three runtime-slot drift rows (39 entries /
56 keys, counted); component-docs-retired-handler-keys-7340.test.ts records the
ruled move of the `?: never` census (22 → 26) and the two names now retired
everywhere (`onClose`, `onSave`); phase2-schemas.test.ts keeps its old
string-callback fixture as the negative reading. content/docs/core/app-schema.mdx
carries the two edits the #7340 pin and check:doc-snippets require.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-litant@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(types): the 58 on* handler keys declared z.function() refuse BY NAME (objectui#6124) - #7339

Merged
os-litant merged 1 commit into
mainfrom
claude/issue-6124-on-handler-keys-refusal-arm
Sep 2, 2026
Merged

fix(types): the 58 on* handler keys declared z.function() refuse BY NAME (objectui#6124)#7339
os-litant merged 1 commit into
mainfrom
claude/issue-6124-on-handler-keys-refusal-arm

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes#6124

What this PR does

The @object-ui/types zod mirrors declared 58 on* handler keys (26 distinct) across 8 files as z.function() — a declaration no JSON document can satisfy on a JSON-authored vocabulary. A JSON author who wrote onClick: { "action": "toast" } was refused with zod's bare invalid_type … expected function, received object, which names the key and nothing else.

This PR executes the maintainer ruling of 2026-08-30 (director seat, batch #8, verbatim 「批次 #8 同意」, comment 5469346437 on #6124): Q1 → A, Q2 → A with C, Q3 → A, Q4 → B.

Clause ②: yes — the published validators' accept set moves (a function value that parsed green is now refused). Contract-review tier: this PR stays draft and carries needs:contract-review.

Census on the base (c93b4d5f3, re-run, not copied)

Anchored ^\s*on[A-Z][A-Za-z]*: z\.function\( over packages/types/src/zod/*.ts: 58 sites, 26 distinct keys, 8 files (complex 10, data-display 8, disclosure 3, feedback 1, form 20, layout 2, navigation 4, overlay 10). All key: z.function( sites: 62 = 58 + the four non-on* (cell, custom, renderCellEditor, validate). Raw z.function( occurrences: 64 = 62 + EventHandlersSchema + one mention inside a describe string. The card's 28/60 and the ruling's 29/61 are both stale; the close condition is this table.

Schema (file)KeyTS twinRuntime consumer measuredDisposition
KanbanSchema (complex)onCardMove, onCardClickcomplex.tsplugin-kanban/index.tsx:193-194 forwards schema.onCardMove / schema.onCardClickruntime slot
KanbanSchema (complex)onColumnAdd, onCardAddcomplex.tsrenderer takes ({ schema }), never reads themretired → ?: never
CalendarViewSchema (complex)onViewChangecomplex.tscalendar-view-renderer.tsx:319pickHostCallbacks(rest) (function values only) → CalendarViewruntime slot
FilterBuilderSchema (complex)onChangecomplex.tsfilter-builder.tsx:26 calls props.onChangeruntime slot
CarouselSchema (complex)onSlideChangecomplex.ts{...props} onto a div; CarouselProps has no such propretired
ChatbotSchema (complex)onError, onSendcomplex.tsplugin-chatbot/renderer.tsx:89,94useObjectChatruntime slot
ChatbotSchema (complex)onSendMessagecomplex.tsrenderer wires its own handleSendMessage; toDomProps drops the keyretired
AlertSchema (data-display)onDismissdata-display.ts{...props} spread onto the Alert element (no such event)retired
ListItemSchema (data-display)onClickListItemlist.tsx renders each item as a plain li with its contentretired
DataTableSchema (data-display)onRowEdit, onRowDelete, onSelectionChange, onColumnsReorderdata-display.tsdata-table.tsx:551,576,1341,1494 call them off schema.*runtime slot
TreeViewSchema (data-display)onSelectChange, onExpandChangedata-display.ts{...props} onto a div; nodes get only onNodeClickretired
AccordionSchema / CollapsibleSchema / ToggleGroupSchema (disclosure)onValueChange / onOpenChange / onValueChangedisclosure.tsleftover props spread onto the Radix root, where the prop is realruntime slot
ToastSchema (feedback)onDismissfeedback.tstoast.tsx takes ({ schema }), never reads itretired
ButtonSchema (form)onClickform.tsbutton.tsx:67toFormControlDomProps forwards it (onClickSDUI_DOM_PASS_THROUGH_KEYS)runtime slot
InputSchema, TextareaSchema, SelectSchema, CheckboxSchema, FileUploadSchema, DatePickerSchema, InputOTPSchema (form)onChangeform.tseach renderer destructures onChange from props and calls itruntime slot
RadioGroupSchema, SwitchSchema, ToggleSchema, SliderSchema, ComboboxSchema (form)onChangeform.tsonly toFormControlDomProps's whitelist is forwarded; no by-name readretired
CalendarSchema (form)onChangeform.ts{...props} onto DayPicker, whose callback is onSelectretired
CommandSchema (form)onChangeform.tslands on cmdk's root div as a DOM change listener (SyntheticEvent) — not the declared (value: string) => voidretired (leak noted)
InputOTPSchema (form)onCompleteform.tswhitelist drops itretired
FormSchema (form)onSubmit, onChange, onCancelform.tsform.tsx:1002-1005 destructures off schema; called at :2008, :1869, :2096runtime slot
CodeEditorSchema (form)onChangeform.tsplugin-editor/index.tsx:48onChange ?? schema.onChangeruntime slot
CardSchema (layout)onClicklayout.tscard.tsx:35,46 reads props.onClick, spreads onto the Card elementruntime slot
TabsSchema (layout)onValueChangelayout.tstabs.tsx:45{...tabsProps} after its own onValueChangeruntime slot
BreadcrumbItemSchema (navigation)onClickBreadcrumbItembreadcrumb.tsx renders links; never reads itretired
SidebarSchema (navigation)onCollapsedChangenavigation.ts{...props} spread onto the Sidebar component; no such propretired
PaginationSchema (navigation)onPageChangenavigation.tspagination.tsx:32 calls props.onPageChangeruntime slot
ButtonGroupButtonSchema (navigation)onClickButtonGroupButtonbutton-group.tsx renders each Button without a click handlerretired
DialogSchema, SheetSchema, DrawerSchema, PopoverSchema, HoverCardSchema, DropdownMenuSchema, AlertDialogSchema (overlay)onOpenChangeoverlay.ts{...props} onto the Radix / vaul root, where onOpenChange is realruntime slot
AlertDialogSchema (overlay)onConfirm, onCanceloverlay.tsfooter wired to schema.onAction and AlertDialogCancel; never readretired
MenuItemSchema (overlay)onClickMenuCommandItemdropdown-menu.tsx:77, context-menu.tsx:75, menubar.tsx:53,67 call item.onClick?.()runtime slot

Totals: 36 runtime slots + 22 retired = 58.

Parity ledger (zod-mirror-parity.test.ts) — every changed row

The mirror's z.input for a refusal arm is undefined, so a runtime-slot key (TS keeps the function) is a measured TYPE drift and lands in KnownDrift; a retired key (?: never) meets undefined on both sides and does not drift. 28 pairs, 35 keys (the 36th, MenuItemSchema.onClick, is a lazy union in EXCLUSIONS). Header counts re-read: 12 → 36 entries, 17 → 52 keys, 146 → 122 pairs with no entry; a class note above ButtonSchema explains the deliberate two-face divergence.

Extended (4): complex.zod.ts#ChatbotSchema: 'body' | 'onError' | 'onSend' · complex.zod.ts#FilterBuilderSchema: 'fields' | 'onChange' · data-display.zod.ts#DataTableSchema: 'rowActions' | 'onRowEdit' | 'onRowDelete' | 'onSelectionChange' | 'onColumnsReorder' · form.zod.ts#FormSchema: 'fields' | 'mode' | 'onSubmit' | 'onChange' | 'onCancel'.

New (24): complex.zod.ts#CalendarViewSchema: 'onViewChange' · complex.zod.ts#KanbanSchema: 'onCardMove' | 'onCardClick' · disclosure.zod.ts#AccordionSchema: 'onValueChange' · disclosure.zod.ts#CollapsibleSchema: 'onOpenChange' · disclosure.zod.ts#ToggleGroupSchema: 'onValueChange' · form.zod.ts#ButtonSchema: 'onClick' · form.zod.ts#CheckboxSchema: 'onChange' · form.zod.ts#CodeEditorSchema: 'onChange' · form.zod.ts#DatePickerSchema: 'onChange' · form.zod.ts#FileUploadSchema: 'onChange' · form.zod.ts#InputOTPSchema: 'onChange' · form.zod.ts#InputSchema: 'onChange' · form.zod.ts#SelectSchema: 'onChange' · form.zod.ts#TextareaSchema: 'onChange' · layout.zod.ts#CardSchema: 'onClick' · layout.zod.ts#TabsSchema: 'onValueChange' · navigation.zod.ts#PaginationSchema: 'onPageChange' · overlay.zod.ts#AlertDialogSchema: 'onOpenChange' · overlay.zod.ts#DialogSchema: 'onOpenChange' · overlay.zod.ts#DrawerSchema: 'onOpenChange' · overlay.zod.ts#DropdownMenuSchema: 'onOpenChange' · overlay.zod.ts#HoverCardSchema: 'onOpenChange' · overlay.zod.ts#PopoverSchema: 'onOpenChange' · overlay.zod.ts#SheetSchema: 'onOpenChange'.

Tests

New pin packages/types/src/__tests__/handler-keys-json-refusal-6124.test.ts (240 cases): anchored source census (0 on* sites; the 4 non-on* sites pinned by name), 58 × {declared on the shape with the #6124 guidance · authored object refused with code: 'custom', path [key], message naming the key, "type" and action:button, message === describe · live function refused · isolated shape green without the key}, wording split (RUNTIME SLOT vs RETIRED), the passthrough counter-probe (omit → green with the object kept), arm ≠ tombstone (custom vs invalid_type), and type-level pins: 22 × RetiredIsNever, 36 × KeepsFunction, both helpers shown able to fail. Compiled by tsconfig.test.jsontsc --listFiles lists the file (544 files).

Three existing tests restated for the accept-set change (verdicts unchanged, messages moved): component-fixture-declared-keys.test.ts counter-probe (still RED, now custom + named guidance), menu-item-union.test.ts (function parses on the TS face only; union arm errors dug), chatbot-authoring-face-keys.test.ts (onSend refused by name on the mirror).

Red-first on the unmodified tree (c93b4d5f3, pin file only): vitest Tests 178 failed | 62 passed (240) — census found 58 sites; object refused with invalid_type not custom; functions parsed GREEN; tsc -p tsconfig.test.json: 22 × TS2344 on RetiredIsNever exactly as predicted.

Union on the final commit 11fd53500 (all through os-verify-lock.sh, verdict lines quoted): pnpm --filter @object-ui/types run type-check (echoed tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json) type-check-exit=0 · pnpm exec vitest run packages/types/ examples/schema-catalog/test/component-fixture-declared-keys.test.ts --maxWorkers=2Test Files 88 passed (88) / Tests 1455 passed (1455) · pnpm --filter @object-ui/types build✓ dist completeness: 1 package(s) complete (118 emitted files verified) · downstream pnpm --filter @object-ui/core run type-check against the rebuilt dist (core's only workspace dep is @object-ui/types) → exit 0.

Ablation (after the commit; trap … EXIT INT TERM; absolute paths): ButtonSchema.onClick reverted to z.function().optional().describe('Click handler') — on-disk proof arm-before=1 mut-before=0 → arm-after=0 mut-after=1, git diff --stat 1/1; no build needed (the pin imports ../zod/form.zod from SOURCE). Result: Tests 6 failed | 234 passed (240) — census (1 site), Button declared/describe, Button object (invalid_type), Button function (GREEN), wording, arm-vs-tombstone — exactly the Button rows. Restore: git checkout HEAD -- ABS_PATH, git diff HEAD empty, git hash-object = git rev-parse HEAD:… = 23a63abe…, git status clean.

Gates (unlocked check:*, verdict lines): check-changeset-presence✅ 21 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s) · check-changeset-no-major✅ No changeset declares a major bump · check:control-bytes✅ OK (scanned 6023 tracked text file(s)) · check:spec-symbols✅ spec symbol derivation: 1333 files scanned · check:doc-types✅ Every documented component type is registered · check:vi-mock-inherit / check:vi-mock-specifiers✅ OK.

Lint, narrowed and measured:eslint --no-inline-config --format json over the 22 staged .ts files → population from eslint's own json output = 22 files, 0 errors, 54 warnings; every warning is @typescript-eslint/no-explicit-any and each warned file's merge-base blob lints to the identical E/W count (complex.ts 0/8, data-display.ts 0/30, form.ts 0/10, complex.zod.ts 0/2, data-display.zod.ts 0/1, navigation.zod.ts 0/2, overlay.zod.ts 0/1) — pre-existing; config invariance: eslint.config.js has no projectService / parserOptions.project, so this diff cannot move an untouched file's verdict. Repo-wide pnpm lint left to CI.

Downstream type-check, narrowed and declared: the only breaking TS change is the 22 ?: never members. A read census over packages/*/src, apps/*/src, examples, scripts for files naming both an affected interface and its retired key found three, none of which assigns the key on that type (AppHeader.tsx builds BreadcrumbItem[] segments without onClick; ObjectCalendar.tsx declares its own local CalendarSchema; chatbot renderer.tsx intersects ChatbotSchema with onSend/onClear, and onSendMessage there is the Chatbot component's own prop). @object-ui/core was type-checked for real against the rebuilt dist; the full turbo run type-check --filter='...@object-ui/types' needs the whole repo built and exceeds the foreground cap on a shared box — CI runs it.

Changeset

.changeset/6124-handler-keys-json-refusal.md@object-ui/types: minor (objectui refuses major); states the accept-set change plainly.

Not in this PR (for the PM)

🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…BY NAME (objectui#6124)
The zod mirrors declared 58 `on*` keys (26 distinct) across eight files as
`z.function()` — a declaration no JSON document can satisfy on a JSON-authored
vocabulary. A JSON author who wrote `onClick: { "action": "toast" }` got zod's
bare `invalid_type … expected function`, naming the key and nothing else.
Ruled shape (maintainer 2026-08-30, batch #8: Q1→A, Q2→A+C, Q3→A, Q4→B):
every site keeps its declaration and becomes a NAMED refusal arm in the #5099
`z.custom` + guidance shape (`handlerKeyRefusal()` in `zod/tombstone.zod.ts`):
the message names the key, says why JSON cannot author it, and points at the
node-type spelling PR #6498 established. The same string is the `.describe()`.
Deletion was measured and refused: under `BaseSchema.passthrough()` an
undeclared key is KEPT, and `onClick` rides `SDUI_DOM_PASS_THROUGH_KEYS` into
the DOM listener slot where React throws at click.
Accept-set change (Clause ②): a live function is refused on the mirror too.
Measured on this tree, the only runtime `safeParse` doors into these mirrors
are the CLI validators and `validateSchema` / `safeValidateSchema`;
`SchemaRenderer` validates through `@object-ui/core`'s structural validator.
TypeScript face, measured per key: `SchemaRenderer` spreads every non-metadata
schema key as a React prop, so 36 keys whose function value reaches a renderer
(read off `schema.*`, called as `props.onX`, or spread onto a Radix root / DOM
listener slot) keep their function type; 22 keys nothing reads carry the
`?: never` tombstone (ADR-0049). The parity ledger records the 35 registered
runtime-slot keys as deliberate drift (28 pairs; `MenuItemSchema` is a lazy
union outside the registry).
Out of scope per the ruling: `cell` / `custom` / `validate` /
`renderCellEditor` stay `z.function()`; `EventHandlersSchema` is objectui#6910.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRRumy89BYdW9ogbcdHTho
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.3 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-CljwiCD4.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.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.72KB2.24KB
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

@os-litant
os-litant marked this pull request as ready for review September 2, 2026 08:49
@os-litant
os-litant added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit a8d5c71Sep 2, 2026
32 checks passed
@os-litant
os-litant deleted the claude/issue-6124-on-handler-keys-refusal-arm branch September 2, 2026 09:04
os-litant added a commit that referenced this pull request Sep 2, 2026
…rable props
objectui#6124 (PR #7339) split the `on*` handler keys in two: 36 runtime
slots keep their function type, and 22 keys nothing reads became `?: never`
tombstones with named refusal arms on the zod mirror. Ten rows across eight
`content/docs` pages went on listing some of the retired 22 as callable
props — a key that is now a `tsc` error to assign and a refusal by name at
authoring time.
The card named three pages (`api/schema-reference.md`,
`data-display/tree-view.mdx`, `form/input-otp.mdx`, five rows); a whole-docs
census resolving each row's `(interface, key)` pair against the shipped
declaration found five more rows on five more pages —
`basic/button-group.mdx` (`ButtonGroupButton.onClick`), and `.onChange` on
`form/calendar.mdx`, `form/combobox.mdx`, `form/command.mdx`,
`form/radio-group.mdx`. A key NAME cannot decide this: `onChange` is retired
on 8 schemas and live on 14, and `input-otp.mdx` carries both dispositions
one line apart.
Seven pages have the retired rows REMOVED. `basic/button-group.mdx` is the
one "marked retired" page and not by taste: `button-group-doc-surface-6347`
asserts set equality between that page's `ButtonGroupButton` block and the
mirror's `.shape`, and a refusal arm is still a key of that shape, so the row
must stay — spelled `never`, with the node-type pointer the tombstone JSDoc
uses. That sibling pin's `onClick` case is updated to read the mirror's
refusal instead of restating the now-stale `() => void`.
The new pin measures the retired population off `packages/types/src` rather
than hand-listing it, flags rows (not prose), and carries six runtime-slot
rows as the blanket-sweep control — two of them on pages this change edits.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRRumy89BYdW9ogbcdHTho
os-project-manager pushed a commit that referenced this pull request Sep 3, 2026
…fuse BY NAME
The objectui#6182 ruling (2026-08-25, option A): the handler-expression string
dialect is not a supported authoring form, on either face. Executed in the
objectui#6124 shape (PR #7339) over the eight sites that PR's `z.function(`
census could not see: four `z.string()` mirrors, three `z.any()` mirrors and
one multi-line `z.function()`.
Per key, measured on origin/main d88e20f:
runtime slot (callable kept, mirror refuses by name):
views.zod.ts DetailViewSchema.onBack — detail-view spreads onto DetailView,
handleBack CALLS onBack(); TS twin
declared `string`, now `() => void`
crud.zod.ts DetailSchema.onBack — register('detail', DetailView)
crud.zod.ts ActionSchema.onClick — ActionRunner awaits action.onClick()
complex.zod.ts CalendarViewSchema.onEventClick — pickHostCallbacks
retired (`?: never`, mirror refuses by name):
app.zod.ts AppActionSchema.onClick — nothing reads AppComponentSchema.actions[]
reports.zod.ts ReportBuilderSchema.onSave / onCancel — no report-builder renderer
crud.zod.ts CRUDDialogSchema.onClose — no crud-dialog renderer
No key was read as a string and dispatched (the card's STOP class did not occur).
The three views.zod.ts event-NAME keys (PR #6899) are untouched; the new pin reads
their describe text as the reason they survive the census.
zod-mirror-parity.test.ts gains the three runtime-slot drift rows (39 entries /
56 keys, counted); component-docs-retired-handler-keys-7340.test.ts records the
ruled move of the `?: never` census (22 → 26) and the two names now retired
everywhere (`onClose`, `onSave`); phase2-schemas.test.ts keeps its old
string-callback fixture as the negative reading. content/docs/core/app-schema.mdx
carries the two edits the #7340 pin and check:doc-snippets require.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-litant@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(types): the 58 on* handler keys declared z.function() refuse BY NAME (objectui#6124) - #7339

Merged
os-litant merged 1 commit into
mainfrom
claude/issue-6124-on-handler-keys-refusal-arm
Sep 2, 2026
Merged

fix(types): the 58 on* handler keys declared z.function() refuse BY NAME (objectui#6124)#7339
os-litant merged 1 commit into
mainfrom
claude/issue-6124-on-handler-keys-refusal-arm

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes#6124

What this PR does

The @object-ui/types zod mirrors declared 58 on* handler keys (26 distinct) across 8 files as z.function() — a declaration no JSON document can satisfy on a JSON-authored vocabulary. A JSON author who wrote onClick: { "action": "toast" } was refused with zod's bare invalid_type … expected function, received object, which names the key and nothing else.

This PR executes the maintainer ruling of 2026-08-30 (director seat, batch #8, verbatim 「批次 #8 同意」, comment 5469346437 on #6124): Q1 → A, Q2 → A with C, Q3 → A, Q4 → B.

Clause ②: yes — the published validators' accept set moves (a function value that parsed green is now refused). Contract-review tier: this PR stays draft and carries needs:contract-review.

Census on the base (c93b4d5f3, re-run, not copied)

Anchored ^\s*on[A-Z][A-Za-z]*: z\.function\( over packages/types/src/zod/*.ts: 58 sites, 26 distinct keys, 8 files (complex 10, data-display 8, disclosure 3, feedback 1, form 20, layout 2, navigation 4, overlay 10). All key: z.function( sites: 62 = 58 + the four non-on* (cell, custom, renderCellEditor, validate). Raw z.function( occurrences: 64 = 62 + EventHandlersSchema + one mention inside a describe string. The card's 28/60 and the ruling's 29/61 are both stale; the close condition is this table.

Schema (file)KeyTS twinRuntime consumer measuredDisposition
KanbanSchema (complex)onCardMove, onCardClickcomplex.tsplugin-kanban/index.tsx:193-194 forwards schema.onCardMove / schema.onCardClickruntime slot
KanbanSchema (complex)onColumnAdd, onCardAddcomplex.tsrenderer takes ({ schema }), never reads themretired → ?: never
CalendarViewSchema (complex)onViewChangecomplex.tscalendar-view-renderer.tsx:319pickHostCallbacks(rest) (function values only) → CalendarViewruntime slot
FilterBuilderSchema (complex)onChangecomplex.tsfilter-builder.tsx:26 calls props.onChangeruntime slot
CarouselSchema (complex)onSlideChangecomplex.ts{...props} onto a div; CarouselProps has no such propretired
ChatbotSchema (complex)onError, onSendcomplex.tsplugin-chatbot/renderer.tsx:89,94useObjectChatruntime slot
ChatbotSchema (complex)onSendMessagecomplex.tsrenderer wires its own handleSendMessage; toDomProps drops the keyretired
AlertSchema (data-display)onDismissdata-display.ts{...props} spread onto the Alert element (no such event)retired
ListItemSchema (data-display)onClickListItemlist.tsx renders each item as a plain li with its contentretired
DataTableSchema (data-display)onRowEdit, onRowDelete, onSelectionChange, onColumnsReorderdata-display.tsdata-table.tsx:551,576,1341,1494 call them off schema.*runtime slot
TreeViewSchema (data-display)onSelectChange, onExpandChangedata-display.ts{...props} onto a div; nodes get only onNodeClickretired
AccordionSchema / CollapsibleSchema / ToggleGroupSchema (disclosure)onValueChange / onOpenChange / onValueChangedisclosure.tsleftover props spread onto the Radix root, where the prop is realruntime slot
ToastSchema (feedback)onDismissfeedback.tstoast.tsx takes ({ schema }), never reads itretired
ButtonSchema (form)onClickform.tsbutton.tsx:67toFormControlDomProps forwards it (onClickSDUI_DOM_PASS_THROUGH_KEYS)runtime slot
InputSchema, TextareaSchema, SelectSchema, CheckboxSchema, FileUploadSchema, DatePickerSchema, InputOTPSchema (form)onChangeform.tseach renderer destructures onChange from props and calls itruntime slot
RadioGroupSchema, SwitchSchema, ToggleSchema, SliderSchema, ComboboxSchema (form)onChangeform.tsonly toFormControlDomProps's whitelist is forwarded; no by-name readretired
CalendarSchema (form)onChangeform.ts{...props} onto DayPicker, whose callback is onSelectretired
CommandSchema (form)onChangeform.tslands on cmdk's root div as a DOM change listener (SyntheticEvent) — not the declared (value: string) => voidretired (leak noted)
InputOTPSchema (form)onCompleteform.tswhitelist drops itretired
FormSchema (form)onSubmit, onChange, onCancelform.tsform.tsx:1002-1005 destructures off schema; called at :2008, :1869, :2096runtime slot
CodeEditorSchema (form)onChangeform.tsplugin-editor/index.tsx:48onChange ?? schema.onChangeruntime slot
CardSchema (layout)onClicklayout.tscard.tsx:35,46 reads props.onClick, spreads onto the Card elementruntime slot
TabsSchema (layout)onValueChangelayout.tstabs.tsx:45{...tabsProps} after its own onValueChangeruntime slot
BreadcrumbItemSchema (navigation)onClickBreadcrumbItembreadcrumb.tsx renders links; never reads itretired
SidebarSchema (navigation)onCollapsedChangenavigation.ts{...props} spread onto the Sidebar component; no such propretired
PaginationSchema (navigation)onPageChangenavigation.tspagination.tsx:32 calls props.onPageChangeruntime slot
ButtonGroupButtonSchema (navigation)onClickButtonGroupButtonbutton-group.tsx renders each Button without a click handlerretired
DialogSchema, SheetSchema, DrawerSchema, PopoverSchema, HoverCardSchema, DropdownMenuSchema, AlertDialogSchema (overlay)onOpenChangeoverlay.ts{...props} onto the Radix / vaul root, where onOpenChange is realruntime slot
AlertDialogSchema (overlay)onConfirm, onCanceloverlay.tsfooter wired to schema.onAction and AlertDialogCancel; never readretired
MenuItemSchema (overlay)onClickMenuCommandItemdropdown-menu.tsx:77, context-menu.tsx:75, menubar.tsx:53,67 call item.onClick?.()runtime slot

Totals: 36 runtime slots + 22 retired = 58.

Parity ledger (zod-mirror-parity.test.ts) — every changed row

The mirror's z.input for a refusal arm is undefined, so a runtime-slot key (TS keeps the function) is a measured TYPE drift and lands in KnownDrift; a retired key (?: never) meets undefined on both sides and does not drift. 28 pairs, 35 keys (the 36th, MenuItemSchema.onClick, is a lazy union in EXCLUSIONS). Header counts re-read: 12 → 36 entries, 17 → 52 keys, 146 → 122 pairs with no entry; a class note above ButtonSchema explains the deliberate two-face divergence.

Extended (4): complex.zod.ts#ChatbotSchema: 'body' | 'onError' | 'onSend' · complex.zod.ts#FilterBuilderSchema: 'fields' | 'onChange' · data-display.zod.ts#DataTableSchema: 'rowActions' | 'onRowEdit' | 'onRowDelete' | 'onSelectionChange' | 'onColumnsReorder' · form.zod.ts#FormSchema: 'fields' | 'mode' | 'onSubmit' | 'onChange' | 'onCancel'.

New (24): complex.zod.ts#CalendarViewSchema: 'onViewChange' · complex.zod.ts#KanbanSchema: 'onCardMove' | 'onCardClick' · disclosure.zod.ts#AccordionSchema: 'onValueChange' · disclosure.zod.ts#CollapsibleSchema: 'onOpenChange' · disclosure.zod.ts#ToggleGroupSchema: 'onValueChange' · form.zod.ts#ButtonSchema: 'onClick' · form.zod.ts#CheckboxSchema: 'onChange' · form.zod.ts#CodeEditorSchema: 'onChange' · form.zod.ts#DatePickerSchema: 'onChange' · form.zod.ts#FileUploadSchema: 'onChange' · form.zod.ts#InputOTPSchema: 'onChange' · form.zod.ts#InputSchema: 'onChange' · form.zod.ts#SelectSchema: 'onChange' · form.zod.ts#TextareaSchema: 'onChange' · layout.zod.ts#CardSchema: 'onClick' · layout.zod.ts#TabsSchema: 'onValueChange' · navigation.zod.ts#PaginationSchema: 'onPageChange' · overlay.zod.ts#AlertDialogSchema: 'onOpenChange' · overlay.zod.ts#DialogSchema: 'onOpenChange' · overlay.zod.ts#DrawerSchema: 'onOpenChange' · overlay.zod.ts#DropdownMenuSchema: 'onOpenChange' · overlay.zod.ts#HoverCardSchema: 'onOpenChange' · overlay.zod.ts#PopoverSchema: 'onOpenChange' · overlay.zod.ts#SheetSchema: 'onOpenChange'.

Tests

New pin packages/types/src/__tests__/handler-keys-json-refusal-6124.test.ts (240 cases): anchored source census (0 on* sites; the 4 non-on* sites pinned by name), 58 × {declared on the shape with the #6124 guidance · authored object refused with code: 'custom', path [key], message naming the key, "type" and action:button, message === describe · live function refused · isolated shape green without the key}, wording split (RUNTIME SLOT vs RETIRED), the passthrough counter-probe (omit → green with the object kept), arm ≠ tombstone (custom vs invalid_type), and type-level pins: 22 × RetiredIsNever, 36 × KeepsFunction, both helpers shown able to fail. Compiled by tsconfig.test.jsontsc --listFiles lists the file (544 files).

Three existing tests restated for the accept-set change (verdicts unchanged, messages moved): component-fixture-declared-keys.test.ts counter-probe (still RED, now custom + named guidance), menu-item-union.test.ts (function parses on the TS face only; union arm errors dug), chatbot-authoring-face-keys.test.ts (onSend refused by name on the mirror).

Red-first on the unmodified tree (c93b4d5f3, pin file only): vitest Tests 178 failed | 62 passed (240) — census found 58 sites; object refused with invalid_type not custom; functions parsed GREEN; tsc -p tsconfig.test.json: 22 × TS2344 on RetiredIsNever exactly as predicted.

Union on the final commit 11fd53500 (all through os-verify-lock.sh, verdict lines quoted): pnpm --filter @object-ui/types run type-check (echoed tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json) type-check-exit=0 · pnpm exec vitest run packages/types/ examples/schema-catalog/test/component-fixture-declared-keys.test.ts --maxWorkers=2Test Files 88 passed (88) / Tests 1455 passed (1455) · pnpm --filter @object-ui/types build✓ dist completeness: 1 package(s) complete (118 emitted files verified) · downstream pnpm --filter @object-ui/core run type-check against the rebuilt dist (core's only workspace dep is @object-ui/types) → exit 0.

Ablation (after the commit; trap … EXIT INT TERM; absolute paths): ButtonSchema.onClick reverted to z.function().optional().describe('Click handler') — on-disk proof arm-before=1 mut-before=0 → arm-after=0 mut-after=1, git diff --stat 1/1; no build needed (the pin imports ../zod/form.zod from SOURCE). Result: Tests 6 failed | 234 passed (240) — census (1 site), Button declared/describe, Button object (invalid_type), Button function (GREEN), wording, arm-vs-tombstone — exactly the Button rows. Restore: git checkout HEAD -- ABS_PATH, git diff HEAD empty, git hash-object = git rev-parse HEAD:… = 23a63abe…, git status clean.

Gates (unlocked check:*, verdict lines): check-changeset-presence✅ 21 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s) · check-changeset-no-major✅ No changeset declares a major bump · check:control-bytes✅ OK (scanned 6023 tracked text file(s)) · check:spec-symbols✅ spec symbol derivation: 1333 files scanned · check:doc-types✅ Every documented component type is registered · check:vi-mock-inherit / check:vi-mock-specifiers✅ OK.

Lint, narrowed and measured:eslint --no-inline-config --format json over the 22 staged .ts files → population from eslint's own json output = 22 files, 0 errors, 54 warnings; every warning is @typescript-eslint/no-explicit-any and each warned file's merge-base blob lints to the identical E/W count (complex.ts 0/8, data-display.ts 0/30, form.ts 0/10, complex.zod.ts 0/2, data-display.zod.ts 0/1, navigation.zod.ts 0/2, overlay.zod.ts 0/1) — pre-existing; config invariance: eslint.config.js has no projectService / parserOptions.project, so this diff cannot move an untouched file's verdict. Repo-wide pnpm lint left to CI.

Downstream type-check, narrowed and declared: the only breaking TS change is the 22 ?: never members. A read census over packages/*/src, apps/*/src, examples, scripts for files naming both an affected interface and its retired key found three, none of which assigns the key on that type (AppHeader.tsx builds BreadcrumbItem[] segments without onClick; ObjectCalendar.tsx declares its own local CalendarSchema; chatbot renderer.tsx intersects ChatbotSchema with onSend/onClear, and onSendMessage there is the Chatbot component's own prop). @object-ui/core was type-checked for real against the rebuilt dist; the full turbo run type-check --filter='...@object-ui/types' needs the whole repo built and exceeds the foreground cap on a shared box — CI runs it.

Changeset

.changeset/6124-handler-keys-json-refusal.md@object-ui/types: minor (objectui refuses major); states the accept-set change plainly.

Not in this PR (for the PM)

🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…BY NAME (objectui#6124)
The zod mirrors declared 58 `on*` keys (26 distinct) across eight files as
`z.function()` — a declaration no JSON document can satisfy on a JSON-authored
vocabulary. A JSON author who wrote `onClick: { "action": "toast" }` got zod's
bare `invalid_type … expected function`, naming the key and nothing else.
Ruled shape (maintainer 2026-08-30, batch #8: Q1→A, Q2→A+C, Q3→A, Q4→B):
every site keeps its declaration and becomes a NAMED refusal arm in the #5099
`z.custom` + guidance shape (`handlerKeyRefusal()` in `zod/tombstone.zod.ts`):
the message names the key, says why JSON cannot author it, and points at the
node-type spelling PR #6498 established. The same string is the `.describe()`.
Deletion was measured and refused: under `BaseSchema.passthrough()` an
undeclared key is KEPT, and `onClick` rides `SDUI_DOM_PASS_THROUGH_KEYS` into
the DOM listener slot where React throws at click.
Accept-set change (Clause ②): a live function is refused on the mirror too.
Measured on this tree, the only runtime `safeParse` doors into these mirrors
are the CLI validators and `validateSchema` / `safeValidateSchema`;
`SchemaRenderer` validates through `@object-ui/core`'s structural validator.
TypeScript face, measured per key: `SchemaRenderer` spreads every non-metadata
schema key as a React prop, so 36 keys whose function value reaches a renderer
(read off `schema.*`, called as `props.onX`, or spread onto a Radix root / DOM
listener slot) keep their function type; 22 keys nothing reads carry the
`?: never` tombstone (ADR-0049). The parity ledger records the 35 registered
runtime-slot keys as deliberate drift (28 pairs; `MenuItemSchema` is a lazy
union outside the registry).
Out of scope per the ruling: `cell` / `custom` / `validate` /
`renderCellEditor` stay `z.function()`; `EventHandlersSchema` is objectui#6910.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRRumy89BYdW9ogbcdHTho
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.3 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-CljwiCD4.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.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.72KB2.24KB
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

@os-litant
os-litant marked this pull request as ready for review September 2, 2026 08:49
@os-litant
os-litant added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit a8d5c71Sep 2, 2026
32 checks passed
@os-litant
os-litant deleted the claude/issue-6124-on-handler-keys-refusal-arm branch September 2, 2026 09:04
os-litant added a commit that referenced this pull request Sep 2, 2026
…rable props
objectui#6124 (PR #7339) split the `on*` handler keys in two: 36 runtime
slots keep their function type, and 22 keys nothing reads became `?: never`
tombstones with named refusal arms on the zod mirror. Ten rows across eight
`content/docs` pages went on listing some of the retired 22 as callable
props — a key that is now a `tsc` error to assign and a refusal by name at
authoring time.
The card named three pages (`api/schema-reference.md`,
`data-display/tree-view.mdx`, `form/input-otp.mdx`, five rows); a whole-docs
census resolving each row's `(interface, key)` pair against the shipped
declaration found five more rows on five more pages —
`basic/button-group.mdx` (`ButtonGroupButton.onClick`), and `.onChange` on
`form/calendar.mdx`, `form/combobox.mdx`, `form/command.mdx`,
`form/radio-group.mdx`. A key NAME cannot decide this: `onChange` is retired
on 8 schemas and live on 14, and `input-otp.mdx` carries both dispositions
one line apart.
Seven pages have the retired rows REMOVED. `basic/button-group.mdx` is the
one "marked retired" page and not by taste: `button-group-doc-surface-6347`
asserts set equality between that page's `ButtonGroupButton` block and the
mirror's `.shape`, and a refusal arm is still a key of that shape, so the row
must stay — spelled `never`, with the node-type pointer the tombstone JSDoc
uses. That sibling pin's `onClick` case is updated to read the mirror's
refusal instead of restating the now-stale `() => void`.
The new pin measures the retired population off `packages/types/src` rather
than hand-listing it, flags rows (not prose), and carries six runtime-slot
rows as the blanket-sweep control — two of them on pages this change edits.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRRumy89BYdW9ogbcdHTho
os-project-manager pushed a commit that referenced this pull request Sep 3, 2026
…fuse BY NAME
The objectui#6182 ruling (2026-08-25, option A): the handler-expression string
dialect is not a supported authoring form, on either face. Executed in the
objectui#6124 shape (PR #7339) over the eight sites that PR's `z.function(`
census could not see: four `z.string()` mirrors, three `z.any()` mirrors and
one multi-line `z.function()`.
Per key, measured on origin/main d88e20f:
runtime slot (callable kept, mirror refuses by name):
views.zod.ts DetailViewSchema.onBack — detail-view spreads onto DetailView,
handleBack CALLS onBack(); TS twin
declared `string`, now `() => void`
crud.zod.ts DetailSchema.onBack — register('detail', DetailView)
crud.zod.ts ActionSchema.onClick — ActionRunner awaits action.onClick()
complex.zod.ts CalendarViewSchema.onEventClick — pickHostCallbacks
retired (`?: never`, mirror refuses by name):
app.zod.ts AppActionSchema.onClick — nothing reads AppComponentSchema.actions[]
reports.zod.ts ReportBuilderSchema.onSave / onCancel — no report-builder renderer
crud.zod.ts CRUDDialogSchema.onClose — no crud-dialog renderer
No key was read as a string and dispatched (the card's STOP class did not occur).
The three views.zod.ts event-NAME keys (PR #6899) are untouched; the new pin reads
their describe text as the reason they survive the census.
zod-mirror-parity.test.ts gains the three runtime-slot drift rows (39 entries /
56 keys, counted); component-docs-retired-handler-keys-7340.test.ts records the
ruled move of the `?: never` census (22 → 26) and the two names now retired
everywhere (`onClose`, `onSave`); phase2-schemas.test.ts keeps its old
string-callback fixture as the negative reading. content/docs/core/app-schema.mdx
carries the two edits the #7340 pin and check:doc-snippets require.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-litant@claude