Uh oh!
There was an error while loading. Please reload this page.
fix(types): the 58 on* handler keys declared z.function() refuse BY NAME (objectui#6124) - #7339
Merged
Merged
Conversation
…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_01NRRumy89BYdW9ogbcdHThoContributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
This was referenced Sep 2, 2026
os-litant
marked this pull request as ready for review
September 2, 2026 08:49
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Sep 2, 2026
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
This was referenced Sep 2, 2026
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#6124
What this PR does
The
@object-ui/typeszod mirrors declared 58on*handler keys (26 distinct) across 8 files asz.function()— a declaration no JSON document can satisfy on a JSON-authored vocabulary. A JSON author who wroteonClick: { "action": "toast" }was refused with zod's bareinvalid_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.
FieldValidationRules在唯一读点既不校验也不归一:pattern.value写成 string(类型明确允许)被 react-hook-form 静默忽略,未识别的规则名同样静默丢弃 #5099 shape (FieldConstraintsSchema.pattern.value,z.custom+ guidance) —handlerKeyRefusal(key, disposition, label)inpackages/types/src/zod/tombstone.zod.ts. The message names the key, says why JSON cannot author it, and points at the node-type spelling PR fix(examples,docs): correct the toast demos to spellings the engine executes #6498 established ({ "type": "toast", ... }/ anaction:buttonnode with a declared action — Q1's option C). One string feeds both channels: the parse-time issue message and the key's.describe()metadata. ⛔ No bare deletion: underBaseSchema.passthrough()an undeclared key is KEPT, andonClickridesSDUI_DOM_PASS_THROUGH_KEYSinto the DOM listener slot where React throws at click — the counter-probe in the new pin holds that hazard.safeParsedoors into these mirrors arepackages/clicheck/validate(JSON files) and the exportedvalidateSchema/safeValidateSchema;SchemaRenderervalidates through@object-ui/core's structuralvalidateSchema, which never reads a handler key. No in-repo path feeds a function-bearing object through these mirrors.SchemaRendererspreads every non-metadata schema key as a React prop (...componentProps,SchemaRenderer.tsx:1576), so a function placed on a schema key reaches the renderer. 36 keys whose value is then read offschema.*, called asprops.onX, or spread onto a Radix root / DOM listener slot keep their function type (JSDoc says why). 22 keys nothing reads carry the?: nevertombstone (ADR-0049; thecrud.tsconfirm/base.tsconvention).cell/custom/validate/renderCellEditorstayz.function()(pinned by name).EventHandlersSchema(base.zod.ts:394) is retire(types):EventHandlersSchema— 公开导出的z.record(z.string(), z.function()),每个值 JSON 不可作者、无任何组合消费、census 与 parity 台账双双结构性不可见(ADR-0049 enforce-or-remove) #6910's, untouched. The three CustomEvent-name keys (z.string(), PR fix(types): the three view handler keys are EVENT NAMES, not callbacks (part of #6124) #6899) are not in the population.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\(overpackages/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). Allkey: z.function(sites: 62 = 58 + the four non-on*(cell,custom,renderCellEditor,validate). Rawz.function(occurrences: 64 = 62 +EventHandlersSchema+ one mention inside adescribestring. The card's 28/60 and the ruling's 29/61 are both stale; the close condition is this table.file)KanbanSchema(complex)onCardMove,onCardClickcomplex.tsplugin-kanban/index.tsx:193-194forwardsschema.onCardMove/schema.onCardClickKanbanSchema(complex)onColumnAdd,onCardAddcomplex.ts({ schema }), never reads them?: neverCalendarViewSchema(complex)onViewChangecomplex.tscalendar-view-renderer.tsx:319pickHostCallbacks(rest)(function values only) →CalendarViewFilterBuilderSchema(complex)onChangecomplex.tsfilter-builder.tsx:26callsprops.onChangeCarouselSchema(complex)onSlideChangecomplex.ts{...props}onto adiv;CarouselPropshas no such propChatbotSchema(complex)onError,onSendcomplex.tsplugin-chatbot/renderer.tsx:89,94→useObjectChatChatbotSchema(complex)onSendMessagecomplex.tshandleSendMessage;toDomPropsdrops the keyAlertSchema(data-display)onDismissdata-display.ts{...props}spread onto theAlertelement (no such event)ListItemSchema(data-display)onClickListItemlist.tsxrenders each item as a plainliwith its contentDataTableSchema(data-display)onRowEdit,onRowDelete,onSelectionChange,onColumnsReorderdata-display.tsdata-table.tsx:551,576,1341,1494call them offschema.*TreeViewSchema(data-display)onSelectChange,onExpandChangedata-display.ts{...props}onto adiv; nodes get onlyonNodeClickAccordionSchema/CollapsibleSchema/ToggleGroupSchema(disclosure)onValueChange/onOpenChange/onValueChangedisclosure.tsToastSchema(feedback)onDismissfeedback.tstoast.tsxtakes({ schema }), never reads itButtonSchema(form)onClickform.tsbutton.tsx:67toFormControlDomPropsforwards it (onClick∈SDUI_DOM_PASS_THROUGH_KEYS)InputSchema,TextareaSchema,SelectSchema,CheckboxSchema,FileUploadSchema,DatePickerSchema,InputOTPSchema(form)onChangeform.tsonChangefrom props and calls itRadioGroupSchema,SwitchSchema,ToggleSchema,SliderSchema,ComboboxSchema(form)onChangeform.tstoFormControlDomProps's whitelist is forwarded; no by-name readCalendarSchema(form)onChangeform.ts{...props}ontoDayPicker, whose callback isonSelectCommandSchema(form)onChangeform.tsdivas a DOM change listener (SyntheticEvent) — not the declared(value: string) => voidInputOTPSchema(form)onCompleteform.tsFormSchema(form)onSubmit,onChange,onCancelform.tsform.tsx:1002-1005destructures offschema; called at:2008,:1869,:2096CodeEditorSchema(form)onChangeform.tsplugin-editor/index.tsx:48onChange ?? schema.onChangeCardSchema(layout)onClicklayout.tscard.tsx:35,46readsprops.onClick, spreads onto theCardelementTabsSchema(layout)onValueChangelayout.tstabs.tsx:45{...tabsProps}after its ownonValueChangeBreadcrumbItemSchema(navigation)onClickBreadcrumbItembreadcrumb.tsxrenders links; never reads itSidebarSchema(navigation)onCollapsedChangenavigation.ts{...props}spread onto theSidebarcomponent; no such propPaginationSchema(navigation)onPageChangenavigation.tspagination.tsx:32callsprops.onPageChangeButtonGroupButtonSchema(navigation)onClickButtonGroupButtonbutton-group.tsxrenders eachButtonwithout a click handlerDialogSchema,SheetSchema,DrawerSchema,PopoverSchema,HoverCardSchema,DropdownMenuSchema,AlertDialogSchema(overlay)onOpenChangeoverlay.ts{...props}onto the Radix / vaul root, whereonOpenChangeis realAlertDialogSchema(overlay)onConfirm,onCanceloverlay.tsschema.onActionandAlertDialogCancel; never readMenuItemSchema(overlay)onClickMenuCommandItemdropdown-menu.tsx:77,context-menu.tsx:75,menubar.tsx:53,67callitem.onClick?.()Totals: 36 runtime slots + 22 retired = 58.
Parity ledger (
zod-mirror-parity.test.ts) — every changed rowThe mirror's
z.inputfor a refusal arm isundefined, so a runtime-slot key (TS keeps the function) is a measured TYPE drift and lands inKnownDrift; a retired key (?: never) meetsundefinedon both sides and does not drift. 28 pairs, 35 keys (the 36th,MenuItemSchema.onClick, is a lazy union inEXCLUSIONS). Header counts re-read: 12 → 36 entries, 17 → 52 keys, 146 → 122 pairs with no entry; a class note aboveButtonSchemaexplains 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 (0on*sites; the 4 non-on*sites pinned by name), 58 × {declared on the shape with the #6124 guidance · authored object refused withcode: 'custom', path[key], message naming the key,"type"andaction: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 (customvsinvalid_type), and type-level pins: 22 ×RetiredIsNever, 36 ×KeepsFunction, both helpers shown able to fail. Compiled bytsconfig.test.json—tsc --listFileslists the file (544 files).Three existing tests restated for the accept-set change (verdicts unchanged, messages moved):
component-fixture-declared-keys.test.tscounter-probe (still RED, nowcustom+ named guidance),menu-item-union.test.ts(function parses on the TS face only; union arm errors dug),chatbot-authoring-face-keys.test.ts(onSendrefused by name on the mirror).Red-first on the unmodified tree (
c93b4d5f3, pin file only): vitestTests 178 failed | 62 passed (240)— census found 58 sites; object refused withinvalid_typenotcustom; functions parsed GREEN;tsc -p tsconfig.test.json: 22 × TS2344 onRetiredIsNeverexactly as predicted.Union on the final commit
11fd53500(all throughos-verify-lock.sh, verdict lines quoted):pnpm --filter @object-ui/types run type-check(echoedtsc --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=2→Test Files 88 passed (88) / Tests 1455 passed (1455)·pnpm --filter @object-ui/types build→✓ dist completeness: 1 package(s) complete (118 emitted files verified)· downstreampnpm --filter @object-ui/core run type-checkagainst 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.onClickreverted toz.function().optional().describe('Click handler')— on-disk proofarm-before=1 mut-before=0 → arm-after=0 mut-after=1,git diff --stat1/1; no build needed (the pin imports../zod/form.zodfrom 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 HEADempty,git hash-object=git rev-parse HEAD:…=23a63abe…,git statusclean.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 jsonover the 22 staged.tsfiles → population from eslint's own json output = 22 files, 0 errors, 54 warnings; every warning is@typescript-eslint/no-explicit-anyand each warned file's merge-base blob lints to the identical E/W count (complex.ts0/8,data-display.ts0/30,form.ts0/10,complex.zod.ts0/2,data-display.zod.ts0/1,navigation.zod.ts0/2,overlay.zod.ts0/1) — pre-existing; config invariance:eslint.config.jshas noprojectService/parserOptions.project, so this diff cannot move an untouched file's verdict. Repo-widepnpm lintleft to CI.Downstream type-check, narrowed and declared: the only breaking TS change is the 22
?: nevermembers. A read census overpackages/*/src,apps/*/src,examples,scriptsfor files naming both an affected interface and its retired key found three, none of which assigns the key on that type (AppHeader.tsxbuildsBreadcrumbItem[]segments withoutonClick;ObjectCalendar.tsxdeclares its own localCalendarSchema; chatbotrenderer.tsxintersectsChatbotSchemawithonSend/onClear, andonSendMessagethere is theChatbotcomponent's own prop).@object-ui/corewas type-checked for real against the rebuilt dist; the fullturbo 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 refusesmajor); states the accept-set change plainly.Not in this PR (for the PM)
content/docs/api/schema-reference.md:926-927(onColumnAdd,onCardAdd),content/docs/components/data-display/tree-view.mdx:46-47(onSelectChange,onExpandChange),content/docs/components/form/input-otp.mdx:36(onComplete). Docs are outside the dispatched file surface; listed for triage, not edited.EventHandlersSchema→ retire(types):EventHandlersSchema— 公开导出的z.record(z.string(), z.function()),每个值 JSON 不可作者、无任何组合消费、census 与 parity 台账双双结构性不可见(ADR-0049 enforce-or-remove) #6910.#6182stays blocked on this card; its close condition is restated by the PM.🤖 Generated with Claude Code
Generated by Claude Code
Generated by Claude Code