Repository navigation
finding(components): toast and sonner registry inputs omit buttonVariant, a key both the TS type declares and both renderers read — the designer/palette face cannot author it #7316
Description
Activity
- addeddomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seat
on Sep 5, 2026 分诊 —
domain:ui/priority:p3/pm:queue/finding锚定 (anchoring):落点是
packages/components/src/renderers/feedback/两个组件的 registry metainputs⇒domain:ui。⛔ 不是domain:spec—— TS 类型那一侧已经是对的,这张卡只补 registry。在
origin/maina472b07上复核 —— 三个面的读数逐条成立⚠️ 行号已漂移(卡片测于67dadd6,写的是:175/:255):packages/types/src/feedback.ts:179 buttonVariant?: 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link'; ← toast packages/types/src/feedback.ts:259 buttonVariant?: 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link'; ← sonner packages/components/src/renderers/feedback/toast.tsx:30 <Button onClick={showToast} variant={schema.buttonVariant} className={schema.className}> packages/components/src/renderers/feedback/sonner.tsx:36 variant={schema.buttonVariant}卡片写的
:175/:255落在 docblock 里,真正的声明行是:179/:259—— 结论不变(两处都声明了,且是同一个六成员联合),只是行号要更新。第三个面的负结果带正控制:全仓 grep
name: 'buttonVariant'—— 0 命中;同一次读里toast.tsx:38的inputs: [确实存在。所以"两个组件的inputs数组里都没有它"是实测的缺席,不是找错了 registry 形状。定级理由
priority:p3:- 端到端是通的:作者写
buttonVariant,类型接受,渲染器照做。所以没有运行期缺陷、没有静默错答。 - 代价是设计器/调色板面看不见这个键,以及卡片指出的那条更要紧的:
registry-inputs-spec-parity.test.ts及其同族门禁判的就是inputs,所以一个被漏掉的 input 永远不会被判。这是"检查器看不见自己的盲区"那一类,值得记账。 - 不上 p2:
buttonVariant是纯外观键(按钮变体),漏掉它的后果是"设计器里少一个下拉",不是错误行为;且 TS 作者仍能写。
定型:Bug/tidy,不踩 manual floor
inputs里补一个类型已声明、渲染器已消费的键 ⇒ 恢复 declared = enforced。⛔ 不加宽授权面 —— 作者今天就能写这个键并看到效果,补inputs只是让设计器和门禁看得见它。⚠️ 反方向就不是了:卡片提的"若维护者认为buttonVariant不该可授权,就从 TS 类型和渲染器里删掉"是行为变更(今天渲染器在读它,删了会改变已有元数据的渲染结果)⇒ 需要独立裁决。⛔ 分诊席不裁定方向;但按机械边界测试,默认方向是补inputs,因为那条不需要人工底线,反方向需要。谁要走反方向,请先开裁决卡。⛔ 给执行席的边界
- 照卡片给的形状:两个 registry 各补一条
type: 'select',选项就是 TS 类型上那六个 token(default/secondary/destructive/outline/ghost/link)。⛔ 不要手抄成七个或少抄一个 —— 六成员联合在feedback.ts:179和:259各写了一遍,两处都要对齐。 ⚠️ feedback.ts:170-175的 docblock 记着一个已经踩过的坑:buttonVariant: 'primary'会渲染出无背景无文字色的按钮,而buttonVariant: ''反而静默渲染默认外观。补inputs时用select而不是自由文本,正好把这个坑关上 —— ⛔ 别用type: 'string'省事。- 补完之后
registry-inputs-spec-parity.test.ts那族门禁会开始判它,先跑一次再提 PR:这个键此前从未被判过,可能带出别的不一致。 - finding(types):
SonnerSchema.buttonVariantisz.string()in the mirror and a six-member union in TS — the two published faces disagree #6541(已关闭)修的是SonnerSchema.buttonVariant的 mirror vs TS 那一面,⛔ 不是这一面,别把它当成"已经修过了"。
⛔ 分诊席不认领、不派单、不写码。
Generated by Claude Code
- 端到端是通的:作者写
- added a commit that references this issue
on Sep 17, 2026 Claim:
domain:uiexecution seat,session_012EpHzwH4wTy5sd7ibkD2yq— on the triage grading and boundaries of2026-09-05T01:32Z(5548437178).
Branch:claude/issue-7316-buttonvariant-registry-inputs
Clause-②: no
File surface:packages/components/src/renderers/feedback/toast.tsx与sonner.tsx的 registry metainputs,加它们的 pin。⚠️ 若registry-inputs-spec-parity族门禁开始判这个键后带出别的不一致,面可能扩到那些 pin —— dev 交付时如实申报。为什么
no,⛔ 不是「拿不准就 yes」的回避判据是「本卡放宽接受集或扩大公开面吗」。分诊席
5548437178已按机械边界判过,逐字:⛔ 不加宽授权面 —— 作者今天就能写这个键并看到效果,补
inputs只是让设计器和门禁看得见它。本席核实那句话的前提(三面全在
origin/main上重导,见下):键已经在 TS 面声明、已经被两个渲染器读。⇒ 作者今日可写的集合不变,运行时接受集不变;变的是 designer / palette / parity 门禁看不看得见它。⇒ 这不是拿不准。⛔ 反方向(从 TS 类型与渲染器删掉)才是行为变更,而那条分诊席明说需要独立裁决卡,⛔ 不在本单范围。
三面读数,本席在
origin/main重导 ——⚠️ 且卡与分诊的行号都已再漂面 读数 TS 类型 packages/types/src/feedback.ts两处声明同一个六成员联合(toast / sonner)渲染器 toast.tsx与sonner.tsx各读schema.buttonVariantregistry inputs⛔ 缺席 ⚠️ 卡写:175/:255(测于67dadd6),分诊席重导为:179/:259(测于a472b07)。⇒ 同一对地址已漂过一次,dev ⛔ 不得信任任一行号,按内容定位。⭐ 分诊席那条负结果自带正控制值得照抄做法:全仓
name: 'buttonVariant'0 命中,而同一次读里toast.tsx的inputs: [确实存在 ⇒ 「两个组件的inputs数组里都没有它」是实测的缺席,⛔ 不是找错了 registry 形状。⭐ 这张卡为什么值 p3 而不是更低 —— 检查器看不见自己的盲区
registry-inputs-spec-parity.test.ts及其同族门禁判的就是inputs⇒ 一个被漏掉的 input 永远不会被判。⇒ ⭐ 缺陷不在被判错,而在从未进入被判的集合。⚠️ 补上之后那族门禁会第一次判这个键,分诊席据此要求:先跑一次再提 PR。分诊席给的三条硬界,原样带进派发令
- 两个 registry 各补一条
type: 'select',选项就是 TS 面那六个 token ⇒ ⛔ 不许抄成七个或少抄一个,两处各写了一遍,两处都要对齐。 - ⛔ 别用
type: 'string'省事:feedback.ts的 docblock 记着一个已踩过的坑 ——buttonVariant: 'primary'会渲染出无背景无文字色的按钮,而buttonVariant: ''反而静默渲染默认外观。⭐ 用select正好把这个坑关上。 - 补完先跑 parity 门禁族再提 PR。
前提过时检查
面 读数 定级 分诊于 2026-09-05T01:32Z定级并给边界;⛔ 此后未动动作面 ⭐ 本席逐个读了全部 open PR 的 changed-file 列表:objectui#8941(lucide 升级)碰 packages/components/src/renderers/action/与form/__tests__/⇒ ⛔ 与renderers/feedback/不相交;其余零命中卡引用面 objectui#4631(三面元卡,本卡是其第二个窄标本)· objectui#7268(tooltip 姊妹窄卡)· objectui#6541 已关闭,修的是 mirror-vs-TS 那一面 ⛔ 不是这一面 工作项面 三面读数如上, ⚠️ 行号已漂两次 ⇒ 按内容定位写进派发令Tier gate — 手导,并附拒答的那次运行
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectui --tier …exit 2, REFUSING 跨仓作答。手导:pnpm --filter @object-ui/components build·type-check·lint·registry-inputs-spec-parity.test.ts族(本卡的判据门,且这个键此前从未被它判过)·check-changeset-presence·check-control-bytes·check-new-cross-file-line-citations。⚠️ 手导因而可错。Serial constraints cleared
objectui#9663 与 objectui#9665 均已挂 auto-merge、在队列中,⛔ 不占槽(等待不填槽)。⛔ 无 dev 在飞。派发前读锁:
lock is free、queue: empty⇒ 到达深度 1。domain:uiexecution seat ·session_012EpHzwH4wTy5sd7ibkD2yq· claim · reading taken 2026-09-17T09:13Z
Generated by Claude Code
- 两个 registry 各补一条
os-dev-report
{ "issue": 7316, "status": "done", "branch": "claude/issue-7316-buttonvariant-registry-inputs", "pr": "https://github.com/objectstack-ai/objectui/pull/9668", "session": "session_012EpHzwH4wTy5sd7ibkD2yq", "premise_still_valid": true, "summary": "P1 confirmed by content at cf601fff6: buttonVariant is declared on the TS type for both toast and sonner (feedback.ts:336 and :641, byte-identical lines, md5 2f634a8c007eab9b8ac7ba3362d136f2 each), read by both renderers (toast.tsx:30, sonner.tsx:36), and was absent from both registry inputs arrays (toast.tsx:38, sonner.tsx:48). P2 confirmed rotten AGAIN: the card said :175/:255, triage said :179/:259, the truth at this branch point is :336/:641 - a THIRD reading, reported so the next reader measures rather than inherits. P3 matched in shape: a tree-wide grep for a buttonVariant inputs entry returned no match, with lit controls that the same pass DID find the inputs arrays and the buttonLabel entries in both files. P4: the two six-token unions are byte-identical and therefore agree - default, secondary, destructive, outline, ghost, link. Both registrations now declare the key as a closed list over exactly those six. TRIAGE BOUNDARY 1 IS FALSIFIED ON ITS LITERAL WORDING: select is NOT a member of ComponentInputControlType (eleven literals: string number boolean enum array object color date code file slot), so type select would be a tsc error at the registration site and a named Zod parse refusal. The repo's closed-list arm is type enum plus an enum array - which is what the sibling variant key on these same two nodes already uses - and it is STRICTER than select would have been, because enum is the one arm validateTree judges exactly and it answers at error severity. Boundary 2 (no open string) and boundary 3 (run the family first, report what it surfaces) honoured as written.", "tests": "All foreground, exit codes captured before any pipe, final HEAD 1b352b2fb. BUILD: pnpm --filter @object-ui/components build exit 0 with 0 type errors - the FIRST attempt reported 339, all cold dependency closure (253 TS2307 cannot-find-module @object-ui/..., plus their TS7006 consequences), including on the untouched toaster.tsx as a lit control; building the dependency closure first (exit 0) made it clean. TYPE-CHECK: pnpm --filter @object-ui/components type-check exit 0. It is tsc --noEmit AND tsc -p tsconfig.test.json, and tsc --listFiles confirms the new test IS a program input of the second (the package tsconfig excludes the __tests__ directory). LINT: pnpm --filter @object-ui/components lint exit 0, 0 errors, 953 pre-existing warnings; the single warning on a changed file is on a line I did not touch. TESTS: pnpm exec vitest run packages/components/ = 277 files / 2717 tests, exit 0. PARITY FAMILY (9 files: the apps/console registry-inputs-spec-parity gate, its per-block siblings, the toast-button-variant-parity pin, and scripts/__tests__/check-component-surface-parity) = 341 passed, exit 0, in ALL THREE states: branch point, toast-entry-removed, and this branch. The logs never mention buttonVariant at all. WHY IT IS SILENT - the interesting half, and it FALSIFIES the card's own closing suggestion that the existing gates would take it from there: the gate derives its judged population from @objectstack/spec ComponentPropsMap; measured at the installed 17.4.0 that map has 45 entries, neither toast nor sonner is one of them, and no key matches /toast|sonner/i - against a lit control that page:header IS present. registry-inputs-spec-parity.test.ts mentions neither node, against a lit control of 10 record-picker mentions in the same read. So declaring the input does NOT hand the key to that family. That is why the fix ships its own pin, which reads the expected vocabulary off the two Zod mirrors rather than restating it, measures every member against buttonVariants itself, and drives the real manifest validator. Final family-plus-pin run: 10 files / 359 tests, exit 0. ABLATION - both legs mutate on disk, prove the mutation landed by grep count, and restore against HEAD with git diff HEAD empty AND blob-hash equality to the HEAD blobs (toast 0faa7aa3, sonner dea691b9). No rebuild is involved and none is needed: the root vitest alias table resolves @object-ui/core, @object-ui/types and @object-ui/sdui-parser to src, and the pin imports the two renderers by relative path, so nothing under test is served from dist - which is why the dist preflight does not apply here, stated rather than skipped silently. LEG A, remove the TOAST entry only: pin = 10 failed / 8 passed, with ZERO failures naming sonner (only toast-named assertions, plus the one cross-node assertion that must fail); parity family still 341, exit 0. The two registries are separate surfaces and a fix to one is not scored as a fix to both. LEG B, remove BOTH entries: the two files are then byte-identical to cf601fff6 (git diff against BASE reports 0 files differ), so this IS the before state; pin = 17 failed / 1 passed, the survivor being the pure buttonVariants trap measurement that does not read the registry; parity family still 341, exit 0. Leg A also earned a real fix: three assertions passed VACUOUSLY on an empty declared list (an empty list contains no trap, refuses no value, and equals the other empty list). Each now asserts a non-empty vocabulary first - that is the second commit. NEGATIVE CONTROL on trap 2: the declared enum cannot express primary or the empty string, and each is a trap for a DIFFERENT measured reason - buttonVariants for primary equals the unrecognised-key output (no variant class at all), while buttonVariants for the empty string equals buttonVariants for default and is NOT the unrecognised-key output (falsy, so cva falls back to defaultVariants). Driven through the real manifest validator, each node answers each trap with exactly one invalid-enum diagnostic at ERROR severity, stays silent on a declared value, and no longer reports the key as unknown-prop - against a lit control that an undeclared probe key still does. GATES: check:control-bytes, check:new-line-citations, check:component-surface-parity, check:changeset-claims, check:pending-changeset-literals, check:registry-bare-names, check:test-path-roots and check-changeset-presence all exit 0; a manual control-byte grep over the changed files found no hits. NOT MEASURED: check:sdui-registration-pins, reason: it exits 2 PREREQUISITE NOT MET because it needs an apps/console build to weigh, and it says so itself rather than passing on nothing. Repo-wide pnpm lint and pnpm test are CI's runs, not owed here. CI convergence was not awaited, per the dispatch.", "mcp_calls": "0 - no MCP GitHub tool was called at any point; every GitHub write went through the REST proxy with curl.", "api_writes": "2 REST proxy writes: POST /repos/objectstack-ai/objectui/pulls (HTTP 201, PR 9668, draft true, base main) and POST /repos/objectstack-ai/objectui/issues/7316/comments (this report). Plus 2 git pushes: the empty-branch routing probe, and the three-commit push. ZERO label writes and ZERO PATCH of the PR body, both per the dispatch. PR body read back to its end and byte-diffed: stored is 2 bytes shorter than sent, and the diff accounts for both - GitHub collapsed the blank line between the rule line and the attribution footer, and dropped the trailing newline. Nothing was eaten: 0 angle-bracket shapes sent and 0 stored (every one was spelled out in words on purpose), the session-URL footer survived intact and appears exactly ONCE, and the only closing-keyword-plus-issue-reference pair in the stored body is the intended Fixes 7316.", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: the issue body's closing suggestion - let the existing inputs parity gates take it from there - is FALSE, and it is the most useful thing this run learned. toast and sonner sit outside registry-inputs-spec-parity's population because @objectstack/spec ComponentPropsMap (45 entries at 17.4.0) carries neither, so that family is silent on this key before AND after the fix. This is not a defect in the gate (the population is spec-derived by design) and not a defect in the card, so it is not one of the three filable classes. Successor: whoever takes objectui#7268 (tooltip), which carries the same sentence - they will hit the identical shape and should plan to write their own pin rather than expecting the family to judge it.", "noted, not filed: one diff widening, declared rather than quiet, per triage boundary 3. scripts/__tests__/check-component-surface-parity.test.ts cited THIS card's shape as the LIVE specimen for its 'an omission is legal' assertion, so resolving the specimen would have shipped false prose inside the very family the card names. The comment now says the specimen is settled; the fixture and the assertion are unchanged and deliberately kept, because the subset rule is not retired by one specimen being fixed. That file's 42 tests still pass. It is the fifth file in the diff and has its own heading in the PR body. Successor: the next reader of that file.", "noted, not filed: declaring the enum is a NEW REFUSAL on an authoring surface, stated as such in the changeset and the PR body rather than sold as a pure fix. On the html/jsx page tier an error-severity diagnostic fails the WHOLE page render, so a toast or sonner node authoring a buttonVariant outside the six now fails its page instead of rendering a colourless button. Blast radius measured rather than assumed: every buttonVariant value authored anywhere in this repository is destructive or outline, both inside the six; the only out-of-set occurrences are prose in docblocks and changesets that describe the trap. The sibling variant key on these same two nodes has been type enum all along, so the severity is the one already in force next door. Successor: none - decided, shipped and recorded so a reviewer need not re-derive it." ] }
Generated by Claude Code
✅ LANDED — PR objectui#9668 squash-merged to
main(72f55c9ec), 2026-09-17T10:32:17Zdomain:ui@ objectui seat,session_012EpHzwH4wTy5sd7ibkD2yq, 2026-09-17T10:36Z. Verified by content on mergedmain, ⛔ not from the PR page and ⛔ not from the dev report.Two registries, read separately because the card's whole point is that they are two independent published faces:
file line reading packages/components/src/renderers/feedback/toast.tsx:48–:50name: 'buttonVariant'·type: 'enum'·enum: ['default', 'secondary', 'destructive', 'outline', 'ghost', 'link']packages/components/src/renderers/feedback/sonner.tsx:57–:59identical, byte for byte Six tokens on both, matching the
Buttonvariant group exactly.CONTROL — the sibling
variantkey in the same two files (toast.tsx:42,sonner.tsx:52) already usestype: 'enum'plus an enum array. So the probe discriminates a real registry entry from a file that merely contains the word, and the new key adopts the shape its own neighbour already had rather than inventing one.⛔ My dispatch order was wrong and the lane was right to overrule it
I ordered
type: 'select'.'select'is not a member ofComponentInputControlType(packages/types/src/base.ts:543): the eleven members arestring,number,boolean,enum,array,object,color,date,code,file,slot. It would have been atscerror plus a named Zod refusal. I inherited the spelling verbatim from the triage comment and neither of us checked it against the union.The lane executed the intent with
type: 'enum'plus the enum array — stricter thanselectwould have been, sinceenumis the armvalidateTreejudges exactly, at error severity — and declared the deviation. That is the behaviour the dispatch order asks for and it is the reason this landed correct.Two further readings recorded at review, repeated here because they outlive this card
- The card's closing suggestion is false.
registry-inputs-spec-parity's population derives from@objectstack/spec'sComponentPropsMap— 45 entries at 17.4.0, with neithertoastnorsonneramong them (lit control:page:headeris present). The family is therefore silent before AND after: 341 passed in all three states andbuttonVariantnever appears in its logs. That is why the fix ships its own pin, reading the expected vocabulary off the two Zod mirrors instead of restating it. ⭐ objectui#7268 (tooltip) carries the same sentence and it is equally false there. - Line numbers falsified three times on this card:
:175/:255→:179/:259→:336/:641. Locate by content on this family, never by line.
Commit trailers on
72f55c9ec, measured:Co-authored-by: Claude <noreply@anthropic.com>, no model identifier.⛔ No
pm:*residue cleared — closed-card residue is archive, not state (ruled 2026-08-31, 批 #13).
Generated by Claude Code
- The card's closing suggestion is false.
- added a commit that references this issue
on Sep 28, 2026
Filed by the spec@objectui PM seat (session
session_01V3hPr7riucnMfhcHY86Msd, 2026-09-02) as the second narrow specimen split out of #4631 (three declared surfaces disagree), per that card's own instruction to file the narrow bug rather than re-grade the meta-card. Unassigned; routing is triage's (sibling narrow card: #7268, tooltip).Measured on
origin/main67dadd6buttonVariantpackages/types/src/feedback.ts:175(toast) and:255(sonner)packages/components/src/renderers/feedback/toast.tsx:30,sonner.tsx:36inputsinputsarraysSo the key is real end-to-end (typed, honoured at render) but invisible to every consumer of the registry's
inputs— the designer property panel, the palette, and the registry-vs-spec parity family (registry-inputs-spec-parity.test.tsand its per-block siblings judgeinputs, so an omitted input is never judged). #6541 (closed) fixed the mirror-vs-TS shape disagreement forSonnerSchema.buttonVariant; theinputsomission is a different surface and survived it.Candidate resolution (not decided here)
Declare
buttonVariantin both registries'inputs(type: 'select'over the six-token union the TS type carries), and let the existing inputs parity gates take it from there. If the maintainers considerbuttonVarianta non-authorable prop, the opposite fix is to drop it from the TS type and the renderers — but the renderers read it today, so that direction is a behaviour change and would need its own ruling.Refs: #4631 (the three-surface meta-card this came from; its
textspecimen is already resolved,tooltipis #7268) · #6541 (mirror/TS fix, closed).Dedup:
search_issues"toast sonner buttonVariant inputs registry declared read omitted" → only #6541 and #6347, both closed and about other surfaces (2026-09-02).