⛔ Correction — this card's headline premise was false
Filed 2026-09-07 against f57ca757b; corrected 2026-09-10 against origin/main 72bcd7783.
The original body inferred inert from the absence of a schema.open read in the renderer. That inference is unsound for this renderer, and the repo has already landed the refutation in prose — packages/components/src/__tests__/alert-dialog-footer-keys-liveness-7963.test.tsx:22-23 cites this card as one of two cases where "called unread" was falsified:
objectui#8236 — CollapsibleSchema.open was called unread and turned out to control expansion, override defaultOpen and freeze the trigger.
The title, the "⇒ inert" conclusion, and the framing of both routes were all wrong. Everything below is re-derived on 72bcd7783. Read the "Not measured" section before acting on this card.
The mechanism, re-derived
1 — open is not stripped. packages/react/src/SchemaRenderer.tsx separates schema metadata from what leaves for the DOM with one destructure:
const {
type: _type, children: _children, body: _body, schema: _schema,
visible: _visible, visibleWhen: _visibleWhen, visibleOn: _visibleOn, visibility: _visibility,
hidden: _hidden, hiddenOn: _hiddenOn, disabled: _disabled, disabledOn: _disabledOn,
dataSource: _dataSource, testId: _testId,
_hidden: __hidden, _disabled: __disabled, responsiveStyles: _responsiveStyles,
...componentProps
} = evaluatedSchema;
open is not on that list — grep -nE '\bopen\b' over the whole destructure block returns 0 hits. So an authored open survives into componentProps, and SchemaRenderer.tsx:1591 spreads it:
...componentProps, // Spread non-metadata schema properties as props
2 — the renderer forwards it, last. packages/components/src/renderers/disclosure/collapsible.tsx:22-23:
({ schema, className, disabled: hostDisabled, ...props }: { schema: CollapsibleSchema; className?: string; disabled?: boolean; [key: string]: any }) => (
<Collapsible defaultOpen={schema.defaultOpen} disabled={hostDisabled} className={className} {...props}>
open lands in ...props and is spread onto Radix Collapsible — and {...props} is written last, so props.open beats the explicitly-written defaultOpen. No schema.open read is needed, which is exactly why the original grep found nothing and concluded the opposite of the truth.
3 — the other half cannot be authored. onOpenChange is a runtime slot the zod twin refuses by name (objectui#6124). So an authored open makes the primitive controlled with no way to change it: the trigger stops working.
⇒ The defect is the inverse of the one filed. open is not a key that validates and does nothing. It is a live key that silently takes over the component, and the only way an author discovers it is that their collapsible stops responding to clicks.
What the card got right: the faces disagree
| face |
declares open? |
where |
| TS interface |
✅ yes |
packages/types/src/disclosure.ts:119-122 — /** Controlled open state */ open?: boolean; |
| zod twin |
✅ yes |
packages/types/src/zod/disclosure.zod.ts:55 — open: z.boolean().optional().describe('Controlled open state') |
registration inputs |
❌ no |
collapsible.tsx:35-44 declares only defaultOpen, disabled, trigger, content, className |
| runtime |
✅ honours it |
via the rest-spread above |
Still a live instance of objectui#4631 (nothing reconciles the three declared surfaces) — but with the registration face as the odd one out, not the runtime, which is the reverse of how the card read it.
Authorship census (conclusion unchanged, re-measured)
git grep -E "(\"type\"|type)\s*:\s*['\"]collapsible['\"]" at 72bcd7783 returns 8 lines in 6 files; one is disclosure.ts:93's discriminant declaration, not an authored node. 7 authored collapsible nodes, in 5 files:
content/docs/components/disclosure/collapsible.mdx
examples/schema-catalog/src/schemas/components-disclosure-collapsible/basic-collapsible.json
packages/components/src/__tests__/disabled-verdict-one-carrier.test.tsx:182
packages/types/src/__tests__/disabled-twin-symmetry-7087.test.ts:205
packages/types/src/__tests__/overlay-trigger-union-7081.test.ts:225, :239, :242
Firing control: all 7/7 carry trigger, so the instrument does read sibling keys off these nodes. open: 0/7.
⇒ nothing in the repo trips this today. The defect is latent, waiting for the first author who reads the interface and believes it.
The decision, re-framed
The original card offered "wire it" vs "retire it" as if the starting state were inert. It is not, so both routes mean something different now:
- A — retire it (ADR-0049 enforce-or-remove). Drop
open from the interface and the zod twin. Retirement alone does not stop the behaviour: it converts a silent takeover into a validation error, which is the point — provided the validator is on the path. If it is not, the key keeps riding the spread and retiring the declaration only removes the author's warning while leaving the takeover. ⚠️ Precondition, not measured here: does the zod refusal gate the render path, or only validate?
- B — wire it properly. Read
schema.open, pass it to Radix, add the missing half. Still blocked exactly where the original card said: onOpenChange is refused by name (objectui#6124), so a JSON author cannot supply it. Ends at "controlled open is not authorable in SDUI".
- C — strip it. Add
open to SchemaRenderer's metadata strip list so it cannot ride the spread. Cheapest way to stop the takeover — and almost certainly wrong, because it strips open for every node type, and open is a real live prop on other primitives (dialog, sheet, popover). Recorded so the next reader does not re-derive and re-reject it.
⇒ still a contract decision, not a mechanical fix. Staying pm:queue; priority unchanged.
⚠️ Not measured on this card
Stated plainly so a dev does not inherit a second unsound premise from me:
- No DOM probe was run for this correction. Steps 1–3 above are code readings, each with the quoted line, and are checkable by anyone. The behavioural half — controls expansion, overrides
defaultOpen, freezes the trigger — is carried from the landed alert-dialog-footer-keys-liveness-7963.test.tsx header, which recorded it as measured. Whoever picks this card should re-measure it through the real SchemaRenderer and the real registry, the way that file re-derived both of its own mechanism halves rather than inheriting them, because line numbers and strip lists move.
- Route A's precondition (whether the zod refusal gates the render path) is unmeasured.
Docs
Unchanged decision, changed reason: still deliberately not documented on content/docs/components/disclosure/collapsible.mdx. It is no longer "don't add a row for an inert key" (objectui#8197's trap) — it is "don't document a key whose behaviour is about to change".
Class — corrected
objectui#6158 (RadioGroupSchema.orientation) and objectui#5667 (CalendarViewSchema's 9 of 13) are the same shape as this card was filed, not as it now reads: those are declared-and-unread. This one is declared-and-live-by-accident, which is objectui#8318's shape (16 keys called unread, 9 measured live), and the inverse of CardSchema.variant (reached its primitive through the spread, had no reader at all). Whoever sweeps objectui#4631 should not treat the three as one bucket — and should not trust a schema.KEY grep to sort them.
The mechanism, re-derived
1 —
openis not stripped.packages/react/src/SchemaRenderer.tsxseparates schema metadata from what leaves for the DOM with one destructure:openis not on that list —grep -nE '\bopen\b'over the whole destructure block returns 0 hits. So an authoredopensurvives intocomponentProps, andSchemaRenderer.tsx:1591spreads it:2 — the renderer forwards it, last.
packages/components/src/renderers/disclosure/collapsible.tsx:22-23:openlands in...propsand is spread onto RadixCollapsible— and{...props}is written last, soprops.openbeats the explicitly-writtendefaultOpen. Noschema.openread is needed, which is exactly why the original grep found nothing and concluded the opposite of the truth.3 — the other half cannot be authored.
onOpenChangeis a runtime slot the zod twin refuses by name (objectui#6124). So an authoredopenmakes the primitive controlled with no way to change it: the trigger stops working.⇒ The defect is the inverse of the one filed.
openis not a key that validates and does nothing. It is a live key that silently takes over the component, and the only way an author discovers it is that their collapsible stops responding to clicks.What the card got right: the faces disagree
open?packages/types/src/disclosure.ts:119-122—/** Controlled open state */ open?: boolean;packages/types/src/zod/disclosure.zod.ts:55—open: z.boolean().optional().describe('Controlled open state')inputscollapsible.tsx:35-44declares onlydefaultOpen,disabled,trigger,content,classNameStill a live instance of objectui#4631 (nothing reconciles the three declared surfaces) — but with the registration face as the odd one out, not the runtime, which is the reverse of how the card read it.
Authorship census (conclusion unchanged, re-measured)
git grep -E "(\"type\"|type)\s*:\s*['\"]collapsible['\"]"at72bcd7783returns 8 lines in 6 files; one isdisclosure.ts:93's discriminant declaration, not an authored node. 7 authored collapsible nodes, in 5 files:content/docs/components/disclosure/collapsible.mdxexamples/schema-catalog/src/schemas/components-disclosure-collapsible/basic-collapsible.jsonpackages/components/src/__tests__/disabled-verdict-one-carrier.test.tsx:182packages/types/src/__tests__/disabled-twin-symmetry-7087.test.ts:205packages/types/src/__tests__/overlay-trigger-union-7081.test.ts:225,:239,:242Firing control: all 7/7 carry
trigger, so the instrument does read sibling keys off these nodes.open: 0/7.⇒ nothing in the repo trips this today. The defect is latent, waiting for the first author who reads the interface and believes it.
The decision, re-framed
The original card offered "wire it" vs "retire it" as if the starting state were inert. It is not, so both routes mean something different now:
openfrom the interface and the zod twin. Retirement alone does not stop the behaviour: it converts a silent takeover into a validation error, which is the point — provided the validator is on the path. If it is not, the key keeps riding the spread and retiring the declaration only removes the author's warning while leaving the takeover.validate?schema.open, pass it to Radix, add the missing half. Still blocked exactly where the original card said:onOpenChangeis refused by name (objectui#6124), so a JSON author cannot supply it. Ends at "controlled open is not authorable in SDUI".opentoSchemaRenderer's metadata strip list so it cannot ride the spread. Cheapest way to stop the takeover — and almost certainly wrong, because it stripsopenfor every node type, andopenis a real live prop on other primitives (dialog, sheet, popover). Recorded so the next reader does not re-derive and re-reject it.⇒ still a contract decision, not a mechanical fix. Staying
pm:queue; priority unchanged.Stated plainly so a dev does not inherit a second unsound premise from me:
defaultOpen, freezes the trigger — is carried from the landedalert-dialog-footer-keys-liveness-7963.test.tsxheader, which recorded it as measured. Whoever picks this card should re-measure it through the realSchemaRendererand the real registry, the way that file re-derived both of its own mechanism halves rather than inheriting them, because line numbers and strip lists move.Docs
Unchanged decision, changed reason: still deliberately not documented on
content/docs/components/disclosure/collapsible.mdx. It is no longer "don't add a row for an inert key" (objectui#8197's trap) — it is "don't document a key whose behaviour is about to change".Class — corrected
objectui#6158 (
RadioGroupSchema.orientation) and objectui#5667 (CalendarViewSchema's 9 of 13) are the same shape as this card was filed, not as it now reads: those are declared-and-unread. This one is declared-and-live-by-accident, which is objectui#8318's shape (16 keys called unread, 9 measured live), and the inverse ofCardSchema.variant(reached its primitive through the spread, had no reader at all). Whoever sweeps objectui#4631 should not treat the three as one bucket — and should not trust aschema.KEYgrep to sort them.