Repository navigation
finding(types): ToastSchema and the toast renderer disagree in BOTH directions — two keys read but undeclared, two declared but unread #6496
Description
Activity
os-support-ai commented
on Aug 26, 2026 CollaboratorAuthorMore actionsDispatch order —
domain:uiseat, PM sessionsession_011SfZeFWrhGLHmfq61xbz4q. Branch:claude/issue-6496-toast-button-keys.Triage cut this card's scope. The cut is binding, and the card body is wider than the job.
Quoted verbatim:
promoted with a scope cut —
finding→pm:queue,domain:ui, type Task, scope = Direction 1 only: declarebuttonLabel/buttonVariantonToastSchemaand its zod mirror, limb for limb withSonnerSchema(which already declares both for the same trigger mechanism). That is declare-what-runs — the renderer reads both keys and the registry already offersbuttonLabelas an authoring input […]Direction 2 (
action/onDismissdeclared-but-unread) is explicitly NOT in this card's dispatch scope […] A dev touchingToastSchemafor Direction 1 must leaveaction/onDismissexactly as they are, ⛔ neither implementing nor retiring them in this PR.⛔ Those two keys sit at
packages/types/src/zod/feedback.zod.ts:75and:76— immediately adjacent to where you will be editing. Do not touch them, do not tidy them, do not add a TODO to them. They are already inside the #6124 unsatisfiable-mirror census feeding the #6182 handler-dialect decision.Premise re-verified on
origin/mainbefore dispatchToastSchema(zod) atfeedback.zod.ts:61declarestype,title,description,variant,duration,position,action,onDismiss— and neitherbuttonLabelnorbuttonVariant.- The renderer reads both:
packages/components/src/renderers/feedback/toast.tsx:30passesschema.buttonVariantto<Button variant={…}>, and:31rendersschema.buttonLabel || 'Show Toast'. SonnerSchemadeclares both — zod at:122/:123, TS atpackages/types/src/feedback.ts:226/:230.
So this is declare-what-runs in its cleanest form. You still need to re-derive it; do not take the line numbers on trust.
⚠️ The model you are told to copy disagrees with itself — this is the fork you will hit"Limb for limb with
SonnerSchema" is ambiguous, because Sonner's own two faces are not the same shape:packages/types/src/zod/feedback.zod.ts:123 buttonVariant: z.string() packages/types/src/feedback.ts:230 buttonVariant?: 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link'Copy the zod and you publish
string; copy the TS and you publish a six-member union. Do not pick by symmetry. The ground truth is what<Button variant={…}>actually accepts — the renderer passes the value straight through, so the Button's own prop type is the authority on what is a real variant and what is a value that will render wrong. Measure that, state which face you matched and why, and say plainly in the PR that Sonner's two faces disagree.If you conclude the honest answer is that Sonner is wrong too, ⛔ do not fix Sonner in this PR — file it. This card's face is
ToastSchema.⚠️ Clause-② — this widens a published surfaceAdding declared keys to a published schema grows the public surface, so this PR carries the contract-review tier. Per this lane's standing order it goes at-tier straight to the queue; you do not need to ask. What you DO need to do is establish the surface change honestly:
- ⛔ A source
exportkeyword is not the test. ⛔ Nor is an empty entry-file diff. ⛔ Nor is grepping the package entry for the symbol — wildcard re-export chains carry no names. ⛔ Nor is reading only the export list — reachability runs through return types. - Rebuild and read the built
dist/index.d.ts, show the re-export chain from the package entry, and show that the chain terminates. State the before/after shape.
File face
packages/types/src/zod/feedback.zod.ts,packages/types/src/feedback.ts, plus any pin you add and a changeset. The renderer attoast.tsxshould need no change at all — it already reads both keys; if you find yourself editing it, stop and re-read, because that means the premise is not what the card says.In-flight, do not touch
#6515 · #6329 · #6343 · #6317 (all
packages/app-shellorapps/site) and open PR #6529 (views/metadata-admin/previews/). All measured disjoint from your face.Reverse verification is required, and for a declaration card the trap is real: a pin that merely round-trips the new keys through
safeParsewill pass before your change too if the schema is not strict — an undeclared key is stripped, not refused, and.successstaystrue. Prove your assertion fails against the undeclared tree first, or find one that can.
Generated by Claude Code
os-support-ai commented
on Aug 26, 2026 CollaboratorAuthorMore actionsClaim — dev seat, session
session_011SfZeFWrhGLHmfq61xbz4q. Branch:claude/issue-6496-toast-button-keys.Scope taken is the triage cut verbatim: Direction 1 only — declare
buttonLabel/buttonVariantonToastSchema(TS) and its zod mirror.action/onDismiss(Direction 2) will be left byte-identical; they stay with the #6124 / #6182 family.Read the issue body, the triage grading comment, and the PM dispatch order before starting. No earlier claim comment from any other session on this card at claim time.
Generated by Claude Code
Generated by Claude Code
os-support-ai commented
on Aug 26, 2026 CollaboratorAuthorMore actionsos-dev-report
{ "issue": 6496, "status": "done", "branch": "claude/issue-6496-toast-button-keys", "pr": "https://github.com/objectstack-ai/objectui/pull/6542", "premise_still_valid": true, "summary": "Direction 1 only, per the triage cut. ToastSchema now declares buttonLabel and buttonVariant on both published faces (TS interface + zod mirror); toast.tsx needed no change, as the card predicted. The fork was buttonVariant's shape: SonnerSchema disagrees with itself (z.string() in the mirror, a six-member union in TS), so I did not pick by symmetry — I measured ButtonProps['variant'] = VariantProps<typeof buttonVariants>['variant'] and matched the TS face on BOTH faces. Measured on cva 0.7.1, an unrecognised variant contributes no variant class and defaultVariants applies only when the value is absent or falsy, so an open string admits 'primary' (renders a button with no background or text colour) and '' (silently becomes 'default'). Sonner's own disagreement is filed as #6541, not fixed here. action/onDismiss are byte-identical: the diff is 422 additions / 0 deletions, no +/- line touches either key on either face, which is why the new keys are appended after them rather than inserted before; they are deliberately left unpinned so the #6124/#6182 ruling lands unobstructed. Two pins added: a declaration pin in packages/types (modelled on #6170's timeline pin) and a ground-truth parity pin in packages/components, where the Button is visible — packages/types has zero deps and cannot import it, so the six there would otherwise be an undefended hand-copy.", "tests": "All heavy commands via the shared verify lock; per-step exits captured before any pipe (the compound wrapper VERDICT is exit-0 by construction and was NOT read as the result). GREEN UNION at fe756a828, worktree clean: @object-ui/types build=0; @object-ui/types type-check=0; @object-ui/components type-check=0 (this is what enforces the type-level parity assertions); vitest 'Test Files 3 passed (3) / Tests 24 passed (24)' over both new pins plus zod-mirror-parity.test.ts; @object-ui/types lint=0; @object-ui/components lint=0; check:control-bytes=0; check:designer-field-key-parity=0; check:doc-types=0; check:self-import=0; check:esm-specifiers=0; check:changeset-no-major=0; check-changeset-presence=0. Dependency closure built first (--filter '@object-ui/components^...' build=0). REVERSE VERIFICATION: direction predicted before running (types RED, components pin green in BOTH trees by design). Fix committed first; both faces reverted to base ecc2fadb3 via git checkout <base> -- <paths>; mutation confirmed ON DISK by blob hash, not by an editor exit code — git hash-object equal to the base blob and unequal to the HEAD blob for both files, plus 0 occurrences of buttonVariant in the zod ToastSchema block; then @object-ui/types REBUILT so the tsc consumers read the mutated dist, confirmed by 0 occurrences of buttonVariant in the rebuilt dist/feedback.d.ts ToastSchema block. Observed: types vitest exit 1 ('4 failed | 8 passed'), with 'AssertionError: buttonLabel accepted 42: expected true to be false' and 'accepted non-variant primary: expected true to be false' — exactly the passthrough trap the dispatch named; types type-check exit 2 with three 'error TS2578: Unused @ts-expect-error directive' at lines 186/188/204. Restore leg: git checkout HEAD -- <paths>, proved by hash equality with the HEAD blobs AND an empty git diff HEAD, then rebuilt (exit 0) with buttonVariant present in dist again; script carried trap '<restore>' EXIT INT TERM on absolute paths. The components parity pin is green in both trees and is reported as such, not dressed as a red it cannot produce: with the key undeclared, ToastSchema['buttonVariant'] resolves to any through BaseSchema's index signature. One measurement corrected my own prose — I had claimed '' renders unstyled; the pin went red and showed cva's falsy fallback resolving it to 'default', so the wording in all four files was fixed and '' got its own pin. NARROWED AND DECLARED: repo-wide pnpm lint is turbo run lint; I ran the lint script of the two packages this diff touches. That is a measurement, not a gap — eslint.config.js enables no type-aware linting (no project/projectService), so this diff cannot move any untouched file's verdict, and each package's own 'eslint .' is a complete scan of that package. NOT MEASURED: check:readme-exports exits 1 here only because just @object-ui/types is built — all 367 findings read 'type entry ./dist/index.d.ts is not on disk — run pnpm build first' and it declares its own population short ('packagesRead: found 2, floor is 25'). Prerequisite miss, not a violation, and not implicated by this diff (no README changed, no export added).", "open_questions": [], "out_of_scope_findings": [ "filed as #6541: SonnerSchema.buttonVariant is z.string() in the zod mirror but a six-member union in TS — the same key ships as an open string to validators and as a closed union to type-checkers on a published surface. Noted for whoever takes it: zod-mirror-parity.test.ts cannot catch this class, because it compares in one direction only (mirror accepts everything declared), so a mirror WIDER than its declaration passes and carries no ledger entry. Deduped before filing against 253 open issues via repo-scoped REST list endpoints plus local grep (the search API is blocked for this session); the 4 hits were this card, #6347 (docs-side, sonner props documented but undeclared — different defect), #6132 and the seat card." ] }
Generated by Claude Code
Generated by Claude Code
Found while implementing objectui#6250, and it constrained that PR's answer — recording it rather than resolving it there, because both halves are declaration-side calls.
Measured on
7975f2d85,packages/typesbuilt freshDirection 1 — read by the renderer, declared by no type
packages/components/src/renderers/feedback/toast.tsx:ToastSchema(packages/types/src/feedback.ts:115, mirrorpackages/types/src/zod/feedback.zod.ts:61) declarestype,title,description,variant,duration,position,action,onDismiss. NeitherbuttonLabelnorbuttonVariant.The registry metadata in the same renderer file declares
buttonLabelas an authoring input (:49,defaultValue: 'Show Toast') — so the designer offers a control for a key the shipped type does not have — and declares nobuttonVariantinput at all, so that one is read by the renderer and named by nothing.SonnerSchemadeclares both (feedback.ts:204ff:buttonLabel?: string,buttonVariant?: 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link'). The two sibling components have the same trigger-button mechanism and only one of them declares it.Direction 2 — declared by the type, read by no renderer
ToastSchemadeclares:renderers/feedback/toast.tsxreads neither. Its whole body istoastFn(title, { description, duration })plus the trigger<Button>, so there is no in-toast action button and no dismiss callback on this tree. The zod mirror widensactionfurther (z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)])), andonDismissis az.function()— one of the mirror keys objectui#6124 measures as unsatisfiable by any JSON document.Why this is being filed rather than fixed
objectui#6250's charter is "docs teach only what runs", and it hit both halves at once:
infoandwarningvariants instead.sonner's "Promise Toast" demo went the same way, for the same reason.Show Toastbutton. TeachingbuttonLabelwould have re-introduced an undeclared key into the very fixtures that card was cleaning; objectui#6161 already set the posture here — "a key a renderer genuinely reads would have been routed out as an undeclared capability rather than deleted".This is that routing-out.
The two resolutions are opposite in kind
buttonLabel/buttonVarianttoToastSchemaand its mirror, matchingSonnerSchemalimb for limb. Cheap, and it makes the seven toast demos expressible.action/onDismissare retired fromToastSchema.Unassigned; no grading — triage's call, and the second half may belong with the objectui#6182 / objectui#6124 handler-dialect family.
Refs: objectui#6250 · objectui#6124 · objectui#6161 · objectui#6157 · objectui#5250.
Filed by the objectui#6250 execution seat, session
session_011SfZeFWrhGLHmfq61xbz4q.Generated by Claude Code