Measured while grading #4433's collision surface (that card's condition 1b: sweep the renderer's other computed props for the same authored-key overwrite exposure). events was crash-capable and is fixed. This one is the remaining exposure, and it is filed rather than fixed because closing it needs a decision the #4433 ruling did not cover.
The exposure
calendar-view's renderer passes onEventClick={handleEventClick} and then spreads {...props} after it. Anything arriving under that key wins — the same shape #4433 fixed for events.
Measured, both channels, before and after #4433's fix (unchanged by it):
| authored |
renders? |
click |
onEventClick: 'NOT-A-FUNCTION' (node key) |
yes, normally |
window error: onEventClick is not a function |
props: { onEventClick: 'NOT-A-FUNCTION' } |
yes, normally |
same |
Note the failure mode is worse than an error boundary, which is why it is not simply cosmetic: React does not route event-handler errors to SchemaErrorBoundary, so this surfaces as an uncaught window error. The calendar keeps looking fine and its click handling is dead.
Why this was not fixed alongside events
The two keys are not the same case, and the difference is what needs deciding.
events is injected metadata: SchemaRenderer forwards a node's events key as a prop, no code in the renderer layer consumes it, and no host has a reason to pass one to this renderer. Dropping it is free.
onEventClick arrives through the same channel for two very different producers:
- an SDUI author writing JSON — the value can never be a function, so it is always broken; and
- a React host rendering
SchemaRenderer schema={…} onEventClick={fn} — the trailing ...props spread makes this work today, and it is the component's genuine escape hatch (the same passthrough MetricWidget deliberately keeps).
The key name cannot separate them; only the value's type can. So the options are:
C and D are the same direction at two altitudes and are probably the real answer; A and B are local patches. This is left for triage rather than guessed at.
Same grading round, for the record: className is not exposed — the renderer destructures it out of its own props, so the rest object cannot carry it, and SchemaRenderer sets className after the node's props container anyway (measured: an authored props.className never reaches the region).
Related: #4433 (the events collision, fixed), #4425 (the phase-2 whitelist question), #4357 / #4428 (the deny-list precedent).
Generated by Claude Code
Measured while grading #4433's collision surface (that card's condition 1b: sweep the renderer's other computed props for the same authored-key overwrite exposure).
eventswas crash-capable and is fixed. This one is the remaining exposure, and it is filed rather than fixed because closing it needs a decision the #4433 ruling did not cover.The exposure
calendar-view's renderer passesonEventClick={handleEventClick}and then spreads{...props}after it. Anything arriving under that key wins — the same shape #4433 fixed forevents.Measured, both channels, before and after #4433's fix (unchanged by it):
onEventClick: 'NOT-A-FUNCTION'(node key)window error: onEventClick is not a functionprops: { onEventClick: 'NOT-A-FUNCTION' }Note the failure mode is worse than an error boundary, which is why it is not simply cosmetic: React does not route event-handler errors to
SchemaErrorBoundary, so this surfaces as an uncaught window error. The calendar keeps looking fine and its click handling is dead.Why this was not fixed alongside
eventsThe two keys are not the same case, and the difference is what needs deciding.
eventsis injected metadata:SchemaRendererforwards a node'seventskey as a prop, no code in the renderer layer consumes it, and no host has a reason to pass one to this renderer. Dropping it is free.onEventClickarrives through the same channel for two very different producers:SchemaRenderer schema={…} onEventClick={fn}— the trailing...propsspread makes this work today, and it is the component's genuine escape hatch (the same passthroughMetricWidgetdeliberately keeps).The key name cannot separate them; only the value's type can. So the options are:
onEventClickunconditionally at the renderer, likeevents. Simple and consistent, but it removes a host path that works today — exactly what plugin-calendar: authoringeventson acalendar-viewnode crashes the component — the SDUI action object overwrites the computed CalendarEvent array #4433's ruling forbade doing silently.onEventClickis not in this widget's registryinputsand not in the spec's node vocabulary, so a publish-time validator could refuse it loudly — contract-first, and it generalizes past this one key to everyon*prop of every widget.toDomPropswhitelist stops atpackages/fields— SDUI widgets elsewhere still close their DOM leak by hand, if at all #4425 phase-2 whitelist. A spread bounded by declaration would close this as a side effect, together with the leak class the sweep measured.C and D are the same direction at two altitudes and are probably the real answer; A and B are local patches. This is left for triage rather than guessed at.
Same grading round, for the record:
classNameis not exposed — the renderer destructures it out of its own props, so the rest object cannot carry it, andSchemaRenderersetsclassNameafter the node'spropscontainer anyway (measured: an authoredprops.classNamenever reaches the region).Related: #4433 (the
eventscollision, fixed), #4425 (the phase-2 whitelist question), #4357 / #4428 (the deny-list precedent).Generated by Claude Code