You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
plugin-calendar: an authored onEventClick reaches CalendarView through the spread and throws an UNCAUGHT error on click — needs a contract decision, not a strip #4453
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:
B. Drop it only when it is not a function. Closes the authored hazard, keeps the host path. But value-type discrimination in a consumer is the lenient-coercion shape AGENTS.md #0.1 rejects, and it makes the rule invisible at authoring time.
C. Reject it at authoring.onEventClick is not in this widget's registry inputs and 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 every on* prop of every widget.
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).
Ruling (delegated authority; open veto window): option D, now concrete — the #4425 phase-2 whitelist contract is ruled (comment 5270759246 there) and its calendar migration resolves this card's A–D question:
onEventClick becomes a DECLARED host escape hatch, function-typed — the fix(plugin-dashboard): declare the DOM pass-through the metric props accept (#4426) #4435 declared-passthrough pattern, which is what makes the hatch legal under the contract: a host-passed FUNCTION is forwarded to CalendarView's prop (the working path the card forbids breaking); a NON-FUNCTION value (the authored-JSON crash case — a string can never be a handler) is dropped = abstain, same answer as absent. This is not option B's lenient-coercion shape because the discrimination now lives in a declared contract at the renderer boundary, not ad-hoc in a consumer — the key is declared, its type is declared, off-type input gets the one answer.
Sweep the renderer's remaining forwarded keys against the same rule; every key is either a declared input (consumed), a declared hatch (typed), or dropped. Name the census in the PR.
Red-first: the card's own measurements — authored onEventClick: 'NOT-A-FUNCTION' (node key AND props container channel, per #4484's lesson) → pre-fix click throws the uncaught onEventClick is not a function window error; post-fix click is a no-op (abstain). Must-not-change: a host-passed function still fires on click (green both sides); the #4433/#4484 pins stay green.
⛔ Sweep/ledger file: widget-dom-leak-sweep.test.tsx is in-flight #4432's surface — do NOT touch it. The now-plantable currentDate canary (post-#4484) and any calendar canary-set extension are REPORTED as a follow-up for #4425's completion pass, not edited here.
Grading: patch @object-ui/plugin-calendar (runtime boundary change; .d.ts-checked — the registry declaration if it moves is measured, minor per precedent if the published surface grows).
Surface (mutual exclusion): packages/plugin-calendar/** + one changeset. Disjoint from in-flight #4474 (organizations), #4336 (detail lookup), #4408 (list error state), #4432 (dashboard + sweep file). #4454 (inert allowCreate) stays its own card — do not fix it here, but if your census confirms it, note it.
Activity
yinlianghui commentedon Aug 12, 2026
CLAIM — session
session_017Qqyix2QcnpUC9XeYVDzx3, branchclaude/issue-4453-calendar-props-contract.Ruling (delegated authority; open veto window): option D, now concrete — the #4425 phase-2 whitelist contract is ruled (comment 5270759246 there) and its calendar migration resolves this card's A–D question:
calendar-view's renderer to consume-or-declare: stop the raw{...props}spread intoCalendarView. Declared registry inputs are consumed (the plugin-calendar: authoringeventson acalendar-viewnode crashes the component — the SDUI action object overwrites the computed CalendarEvent array #4433eventsstrip and fix(plugin-calendar): authored ISO currentDate is parsed to a Date instead of crashing the calendar (#4452) #4484currentDateparse already are — this completes the set); injected props consumed per the contract.onEventClickbecomes a DECLARED host escape hatch, function-typed — the fix(plugin-dashboard): declare the DOM pass-through the metric props accept (#4426) #4435 declared-passthrough pattern, which is what makes the hatch legal under the contract: a host-passed FUNCTION is forwarded toCalendarView's prop (the working path the card forbids breaking); a NON-FUNCTION value (the authored-JSON crash case — a string can never be a handler) is dropped = abstain, same answer as absent. This is not option B's lenient-coercion shape because the discrimination now lives in a declared contract at the renderer boundary, not ad-hoc in a consumer — the key is declared, its type is declared, off-type input gets the one answer.Red-first: the card's own measurements — authored
onEventClick: 'NOT-A-FUNCTION'(node key ANDpropscontainer channel, per #4484's lesson) → pre-fix click throws the uncaughtonEventClick is not a functionwindow error; post-fix click is a no-op (abstain). Must-not-change: a host-passed function still fires on click (green both sides); the #4433/#4484 pins stay green.⛔ Sweep/ledger file:
widget-dom-leak-sweep.test.tsxis in-flight #4432's surface — do NOT touch it. The now-plantablecurrentDatecanary (post-#4484) and any calendar canary-set extension are REPORTED as a follow-up for #4425's completion pass, not edited here.Grading: patch
@object-ui/plugin-calendar(runtime boundary change;.d.ts-checked — the registry declaration if it moves is measured, minor per precedent if the published surface grows).Surface (mutual exclusion):
packages/plugin-calendar/**+ one changeset. Disjoint from in-flight #4474 (organizations), #4336 (detail lookup), #4408 (list error state), #4432 (dashboard + sweep file). #4454 (inertallowCreate) stays its own card — do not fix it here, but if your census confirms it, note it.Generated by Claude Code
Generated by Claude Code
fix(plugin-calendar): calendar-view consumes or declares every forwar…
docs(components): name the shipped handler on five action props
test(types): split the unmirrored ledger by remedy — 23 runtime-only …
docs(components): name the shipped handler on five action props (obje…
test(types): split the unmirrored ledger by remedy — 23 runtime-only …