Found while implementing objectui#6496 (declare buttonLabel / buttonVariant on ToastSchema). That card's dispatch told the dev to match SonnerSchema "limb for limb"; the model turned out to disagree with itself, so the shape had to be derived from the Button instead. Recording the sibling's defect rather than fixing it there — objectui#6496's face is ToastSchema, and narrowing a second published key is its own accept-set change with its own consumers to measure.
Measured on ecc2fadb3, packages/types built fresh
packages/types/src/zod/feedback.zod.ts:136 buttonVariant: z.string().optional().describe('Action button variant'),
packages/types/src/feedback.ts:230 buttonVariant?: 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link';
Both are published: @object-ui/types re-exports SonnerSchema from dist/index.d.ts, and @object-ui/types/zod re-exports the mirror from dist/zod/index.zod.d.ts. So the same key on the same component ships as an open string to anyone validating, and as a closed six-member union to anyone type-checking.
Why the open string is wrong, not merely wide
renderers/feedback/sonner.tsx:36 passes the value straight through to <Button variant={…}>, whose prop type is VariantProps<typeof buttonVariants>['variant'] — exactly the six the TS face lists. Measured on cva 0.7.1, an unrecognised key contributes no variant class, and defaultVariants applies only when the value is absent or falsy:
buttonVariants({ variant: undefined }) -> "… bg-primary text-primary-foreground …" default look
buttonVariants({ variant: 'ghost' }) -> "… hover:bg-accent …" real variant
buttonVariants({ variant: 'primary' }) -> "…" NO colour at all
buttonVariants({ variant: '' }) -> "… bg-primary …" silently 'default'
So buttonVariant: 'primary' — the likeliest wrong spelling, since the default variant's own class is bg-primary — validates green against the mirror and renders a button with no background and no text colour. And buttonVariant: '' validates green and is silently reinterpreted as default, which is the value that does not even look wrong.
Why nothing caught it
packages/types/src/__tests__/zod-mirror-parity.test.ts compares each mirror against its declaration in one direction only — "the mirror accepts everything the TS declaration declares". A mirror that is wider than its declaration passes, which is why feedback.zod.ts#SonnerSchema carries no KnownDrift / UnmirroredDeclared entry despite this. The census direction is deliberate and this is not a bug in it; it just means the wider-than-declared class is currently unmeasured across all 158 pairs.
Suggested handling
Narrow the mirror to z.enum(['default', 'secondary', 'destructive', 'outline', 'ghost', 'link']), matching the TS face and the Button. objectui#6496 landed exactly that spelling on ToastSchema for the same trigger mechanism, so the shape is already settled and pinned — packages/components/src/__tests__/toast-button-variant-parity.test.ts pins the six against ButtonProps['variant'] in both directions, and would cover SonnerSchema with a one-line addition.
It is an accept-set narrowing on a published surface: values outside the six start being rejected. Every one of them renders a colourless or silently-defaulted button today, so nothing that renders correctly stops rendering — but it is a real narrowing and wants its own card rather than a rider.
Unassigned; no grading — triage's call.
Refs: objectui#6496 · objectui#6347 · objectui#6124.
Generated by Claude Code
Found while implementing objectui#6496 (declare
buttonLabel/buttonVariantonToastSchema). That card's dispatch told the dev to matchSonnerSchema"limb for limb"; the model turned out to disagree with itself, so the shape had to be derived from the Button instead. Recording the sibling's defect rather than fixing it there — objectui#6496's face isToastSchema, and narrowing a second published key is its own accept-set change with its own consumers to measure.Measured on
ecc2fadb3,packages/typesbuilt freshBoth are published:
@object-ui/typesre-exportsSonnerSchemafromdist/index.d.ts, and@object-ui/types/zodre-exports the mirror fromdist/zod/index.zod.d.ts. So the same key on the same component ships as an open string to anyone validating, and as a closed six-member union to anyone type-checking.Why the open string is wrong, not merely wide
renderers/feedback/sonner.tsx:36passes the value straight through to<Button variant={…}>, whose prop type isVariantProps<typeof buttonVariants>['variant']— exactly the six the TS face lists. Measured oncva0.7.1, an unrecognised key contributes no variant class, anddefaultVariantsapplies only when the value is absent or falsy:So
buttonVariant: 'primary'— the likeliest wrong spelling, since the default variant's own class isbg-primary— validates green against the mirror and renders a button with no background and no text colour. AndbuttonVariant: ''validates green and is silently reinterpreted asdefault, which is the value that does not even look wrong.Why nothing caught it
packages/types/src/__tests__/zod-mirror-parity.test.tscompares each mirror against its declaration in one direction only — "the mirror accepts everything the TS declaration declares". A mirror that is wider than its declaration passes, which is whyfeedback.zod.ts#SonnerSchemacarries noKnownDrift/UnmirroredDeclaredentry despite this. The census direction is deliberate and this is not a bug in it; it just means the wider-than-declared class is currently unmeasured across all 158 pairs.Suggested handling
Narrow the mirror to
z.enum(['default', 'secondary', 'destructive', 'outline', 'ghost', 'link']), matching the TS face and the Button. objectui#6496 landed exactly that spelling onToastSchemafor the same trigger mechanism, so the shape is already settled and pinned —packages/components/src/__tests__/toast-button-variant-parity.test.tspins the six againstButtonProps['variant']in both directions, and would coverSonnerSchemawith a one-line addition.It is an accept-set narrowing on a published surface: values outside the six start being rejected. Every one of them renders a colourless or silently-defaulted button today, so nothing that renders correctly stops rendering — but it is a real narrowing and wants its own card rather than a rider.
Unassigned; no grading — triage's call.
Refs: objectui#6496 · objectui#6347 · objectui#6124.
Generated by Claude Code