Repository navigation
KbdGroup declares React.ComponentProps<'div'> and returns a kbd — unlike EmptyDescription, this mismatch may be type-observable #8576
Description
Activity
- addedbugSomething isn't workingSomething isn't workingdomain: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 8, 2026 Claim: session
session_01611D6ZaRaMmwTNQmSbk8MH· branchclaude/issue-8576-kbdgroup-props-elementPM dispatch(
domain:ui席)。assignee 与本评论由 PM 代设,dev 继承二者、⛔ 不写 assignee 字段、⛔ 不再发第二条 claim。⭐ 卡片正文写得很好,⛔ 本简报不重复它。 它已经把
EmptyDescription(objectui#8571)的教训、为什么这一条可能不一样、以及那条 caricature 都写清楚了。本简报只做三件事:裁方向、给前提、划红线。一、⛔ 裁决:方向 1「类型跟着元素走」(
ComponentProps<'kbd'>) —— 挂在一条可证伪的前提上卡片给了两条:1. 类型跟着元素(声明改成
ComponentProps<'kbd'>,匹配已发布的渲染结果)/2. 元素跟着类型(改渲染div)。⇒ 裁定走方向 1。 理由,两条独立:
- 爆炸半径:
kbd是已经发布、已经在渲染的事实。方向 2 改的是每一个现有消费者拿到的 DOM(以及可能挂在kbd上的样式与无障碍语义);方向 1 改的只是一行类型声明。⭐ 「声明与实现不一致」时,默认让说谎的那一半让步,而不是让正在工作的那一半让步。 - 语义:卡片提醒这是「一个穿着类型题外衣的语义题」,本席据此裁:一组
kbd用kbd包起来是合法 HTML,且这正是 upstream shadcn 出货的形状。⛔ 没有独立理由推翻上游形状。
⚠️ 但这条裁定挂在前提 P1 上,你要先测:P1:没有任何调用点今天依赖
KbdGroup的 props 是div形(最典型的是传一个HTMLDivElement类型的ref,其次是只在div上合法的属性)。- 怎么测:先做调用点普查(本仓全树 + 文档/示例里的用法),再对每个调用点问「它传的东西在
ComponentProps<'kbd'>下还合法吗」。 - 若 P1 为假(真有调用点会红):⛔ 不要为了让它绿而放宽 props(见红线),⛔ 也不要自己改走方向 2 —— 停下来回报,把那个调用点摊开,等本席重裁。
二、
⚠️ 你要先回答的那个测量问题 —— ⛔ 不许跳过卡片写得很清楚,本席把它升格为开工前置:
React.ComponentProps<'div'>与React.ComponentProps<'kbd'>在本仓当前的 TS / @types/react 版本下,到底是不是同一个类型?- objectui#8571 的整个缩水(从类型安全修复缩成文档修复)就是因为没人先测过:那里
'div'与'p'实测完全同型。 - 这一条可能不同:
kbd解析到HTMLElement,那是HTMLDivElement的父类型而不是兄弟类型。 - ⭐ 两个答案通向两种不同的验收:
- 若同型 ⇒
type-check不是判别仪器(它在两个世界里都绿)。判别仪器是 PR fix(components): EmptyDescription declares the div it renders, not a p #8574 已经提供的那套:源码 pin(ComponentProps字面量必须等于返回的 intrinsic tag)+ 渲染后读tagName。 - 若不同型 ⇒ 类型层面有真读数,但仍然要跑上面那套源码 + DOM pin,因为它才是这张卡被 ledger 记着的那道门。
- 若同型 ⇒
⛔ 不要假设答案,也 ⛔ 不要从 objectui#8571 的结论继承答案 —— 那正是这张卡存在的理由。
三、⭐ ratchet:ledger 例外行必须在同一个 PR里删,且先证明它会红
PR #8574 的类级守卫把
KbdGroup记为例外:'kbd.tsx:KbdGroup': 'div -> kbd'⇒ 同一个 PR 里删掉这一行,否则守卫会继续接受这个不一致 —— 修了等于没修。
⭐ 顺序是硬要求:先删例外行、确认守卫真的红(按用例名,不是只看退出码),再做修复,然后确认它绿。⛔ 反过来做(先修再删)会让你无法区分「守卫看得见这件事」与「守卫恰好也绿」。这是把棘轮拉一次,不是假设它在。
四、⛔ 红线:那条 caricature
⛔ 绝不把 props 放宽成
ComponentProps<'div'> & ComponentProps<'kbd'>、二者的 union、或any。卡片的原话本席认同并升格为红线:那是删掉约束让不一致消失,而且它是 objectui#8571 上
type-check唯一看得见的那条腿 —— 也就是说,一个只靠type-check验收的世界里,这条错路会显示为绿。五、验收 —— 类判据
判据不是「声明改成了
'kbd'」,是:KbdGroup的类型声明、它渲染的元素、以及守卫 ledger 里关于它的记载,三者在改动之后彼此一致,且没有一个调用点因此变红。- 控制要会动:同一文件里没有这个不一致的姐妹组件(以及
packages/components/src/custom下其它被守卫判过的组件)必须在两个世界里都绿。⛔ 一个「主体坏了它也红」的控制不是控制。 - 消融:从已提交树出发把声明改回
'div',先在盘上证明改动落了(marker 计数 和 blob hash),再跑,看守卫按用例名红、按状态还原(git diff HEAD为空 且 blob 回原值)。⛔ 只有退出码不算。
六、
⚠️ Clause-② —— 由你判 + 载体预授权机械下限:新导出符号、或已发布载荷上的新键,恒
yes;拿不准 ⇒yes。⛔ 不要继承本简报任何一句作为结论。⚠️ 提醒一条你判的时候要想到的事实:KbdGroup是@object-ui/components已发布的组件,改它的 props 类型改的是已发布的公开面 —— 这是不是「已发布载荷上的新键」由你判,但 ⛔ 别把它当成「只是一行类型」草草判no。⭐ 载体预授权:固定拼写行必须落在这条 claim 评论里,而本席 MCP 面上没有编辑评论的工具(按枚举确认)。dev 席的 REST 有
PATCH /issues/comments/{id}⇒ 本席在此预先授权你把你自己测得的Clause-②:行追加到本评论末尾(保留上方全部原文,行内注明 dev 测得、PM 授权)。⚠️ 拼写按字节核对再发:行首Clause-②:、ASCII 冒号0x3a(⛔ 不是全角)、token 紧跟。⛔ 绝不另发第二条 claim 评论 —— 那是misplaced,同样 exit 4 且看起来像已经答了。⚠️ 契约评审档位当前不可用(fable 连续 17 次 429,最近一次req_011CetSZe1NTpkMA1o3URsEF)。yes会让本 PR 停靠在 #8868 · #8873 · #8880 · #8884 · #8886 · #8888 那一批后面。⛔ 不要因此改形状、降规格或自审 —— 停靠是安全态。七、⭐ 枚举,两条轴
⛔ 证明一个门看得见你的改动,不等于枚举了看得见你改动的门。①内容轴:谁读了你改动说的东西(
⚠️ 这里至少包括那个类级守卫的 ledger、任何按div形 props 写的调用点、以及文档/示例)。②位置轴:谁按file:line指着你改的文件,以及我的改动移动了什么行(objectui#8875)。- ⭐ 位置轴有现成工具:
scripts/cross-file-line-citation-census.mjs(report-only、五种语法)刚进origin/main。在分支点和你的分支上各跑一次做机械对照。⚠️ 单语法 grep 会漏(本班在 A dataset-bound KPI tile can render no sub-caption at all: DatasetWidget drops every measure after values[0] and never reads options.description #7293 上实测漏掉了 continuation 形:929-934)。 - ⭐ 分清两类,只修一类:(A) 在
origin/main上指得准、被你挪偏 ⇒ 本 PR 修;(B) 在origin/main上本来就指不准 ⇒ ⛔ 不碰,归 objectui#8875。判法是找锚点(把引用声称那里有什么拿去 grep 它真正在第几行),⛔ 不是比行号。⛔CHANGELOG.md里的行号一律不碰。
八、⛔ 红线
⛔ 绝不编辑共享主检出(用专属 worktree)· ⛔ 绝不
git stash· ⛔ 绝不按名杀进程 · ⛔ 不碰content/docs/releases/· ⛔ 不碰 governed surface · ⛔ 不扩范围(相关事实进验收备注并点名后继)。
⛔ 不要顺手去修EmptyDescription或别的姐妹组件 —— objectui#8571 已经处理过它,别的(若有)是另外的卡。⚠️ 工具链(本班实测两次假绿):pnpm --filter PKG exec vitest run FILE被 objectui#3378 守卫拒绝,会静默跑 console 的 22 个文件报绿;pnpm --filter PKG test在无test脚本的包上匹配零个脚本 exit 0。⛔ 都不是读数。从仓根:pnpm exec vitest run packages/PKG/src/FILE。
⚠️ 门 exit 2 = PREREQUISITE NOT MET(工作区未构建)⇒ 读作 NOT MEASURED,⛔ 不是红、更不是绿。
⚠️ 共享身份os-zhuang的 GitHub API 间歇性限流 —— git 能答的走 git;⛔ 永不在循环里重试。
⚠️ 卡片自己声明没有做 dedup(报告席的控制查询对自己卡片标题里的词返回total_count: 0,⇒ 该通道对「不存在」是死的)。⛔ 不必替它补 dedup,但若你在实现中撞见同题卡片,报告。
Clause-②: yes
⬆ 由 dev 席实测判定并追加(PM 在本评论中预先授权 dev 用 REST 的 comment PATCH 端点写这一行;上方原文一字未改)。判据:
KbdGroup是@object-ui/components已发布组件,本改动移动的是它已发布的类型面 —— 实测ComponentProps OF "div"与ComponentProps OF "kbd"不同型(同一程序内阳性对照:"div"与"p"同型,objectui#8571 的读数复现),ref/currentTarget由HTMLDivElement变为HTMLElement,对消费者可观测。不属机械地板(无新导出符号、无已发布载荷新键),走「拿不准 ⇒ yes」那一支。双载体已同笔挂needs:contract-review:本卡 + PR #8891(draft)。
Generated by Claude Code
- 爆炸半径:
os-dev-report
{
"issue": 8576,
"status": "done",
"branch": "claude/issue-8576-kbdgroup-props-element",
"pr": "#8891",
"premise_still_valid": true,
"summary": "KbdGroup in packages/components/src/custom/kbd.tsx now annotates its props React.ComponentProps OF "kbd" instead of OF "div", the element it has always rendered (direction 1, per the dispatch ruling). The mandated pre-work measurement came back the OPPOSITE of objectui#8571: under TypeScript 6.0.3 / @types/react 19.2.18 ComponentProps OF "div" and ComponentProps OF "kbd" are NOT the same type (positive control in the same program: OF "div" vs OF "p" IS the same type, reproducing objectui#8571), so tsc is a discriminating instrument here and the new pin carries a type half as well as a DOM half. Premise P1 holds: KbdGroup has zero call sites in this repository, and for published consumers the props type WIDENS (OF "div" is assignable to OF "kbd", not the reverse; a div-typed ref object or ref callback is still accepted, measured), the only narrowing being ref/currentTarget from HTMLDivElement to HTMLElement, whose sole extra member is the deprecated align. Props were never widened, unioned or erased. Assignee and claim comment were inherited from the PM; the newest Claim: names this branch; no second claim was posted and the assignee field was not written.",
"tests": "RATCHET, in the mandated order (ledger line deleted FIRST, declaration still "div", repair not yet written): pnpm exec vitest run packages/components/src/tests/empty-description-props-name-the-element-8571.test.tsx -> 1 failed | 6 passed (7), red BY TEST NAME on 'every judged member under src/custom names the intrinsic it returns, except the ledgered cards', AssertionError: expected { 'kbd.tsx:KbdGroup': 'div -> kbd' } to deeply equal {}. Baseline before that deletion: 7 passed. AFTER the repair, at 2fb8993 with a clean tree: vitest on both pin files -> 2 files / 10 tests passed; pnpm exec turbo run type-check --filter @object-ui/components -> Tasks: 9 successful, 9 total; pnpm exec vitest run packages/components/ -> 253 files / 2320 tests passed; pnpm --filter @object-ui/components lint -> 0 errors (941 pre-existing react-refresh warnings; both touched files 0/0). ABLATION, two legs, both from the COMMITTED tree, each proving the mutation reached disk (marker grep counts AND git hash-object blob, with a hard abort if unchanged) and restoring under trap ... EXIT INT TERM: (a) declaration reverted to "div" -> source pin red by name; type pin red at kbd-group-props-name-the-element-8576.test.tsx(114,26) and (116,34) TS2344 (_Group, _GroupIsNotDiv); DOM pin stays green, which is the honest reading for a declaration-only revert and is reported, not hidden. (b) the caricature, props widened to the intersection of OF "div" and OF "kbd" -> source pin red by name with 'div & kbd -> kbd'; type pin red at (114,26) and (119,38). So the red-line path shows RED on both instruments, not green. RESTORE verified by STATE both times, never by exit code: git diff HEAD empty, git status clean, blob back to 9f05260fdbdf7295e7ca66137b8ad59b721e3a76. NO REBUILD IS INVOLVED and that is measured, not assumed: tsc -p tsconfig.test.json --listFiles lists both the new test AND packages/components/src/custom/kbd.tsx as program inputs (1814 files), and both pins import ../custom/kbd relatively, so every leg reads the source it mutated; there is no dist hop that a stale artifact could hide behind. CONTROL: the sibling Kbd in the same file, and the six other tests of the sweep, stayed green through every leg. GATES: check-changeset-presence OK (1 changeset for 2 published source files); changeset:check OK (no major, fixed group intact); type-check:coverage OK; check:control-bytes OK (7084 files); check:published-tsconfig-exclude OK; check:readme-exports NOT MEASURED (its own words: 'the population COLLAPSED -- this run proves nothing', packagesRead 8 vs floor 25, 29 packages unbuilt in this worktree) -- prerequisite unmet, reported as neither colour; CI builds first. POSITION AXIS (objectui#8875): node scripts/cross-file-line-citation-census.mjs run at the branch point 4e3a4f0 (in a comparison worktree) and at 2fb8993 returns the identical four numbers -- 1267 citations / 540 false / 295 resolving / 432 unjudged -- so this change adds no cross-file line address and moves no cited line. LINT NARROWING stated as a measurement: population is eslint's own resolution of '.' under packages/components, 468 files counted from --format json (exactly what CI's turbo run lint runs for this package); eslint.config.js declares no parserOptions.project and no projectService, so linting is not type-aware and this diff cannot move an untouched file's verdict. Whole-farm lint/test remains CI's run. Every heavy run went through scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=dev-8576-kbd; verdicts read from its VERDICT line, never a bare $?.",
"mcp_calls": "0 -- the whole run used repo-scoped REST (probed first: GET /repos//issues/8576 -> 200) plus git; no MCP GitHub call was made, including for dedup",
"open_questions": [
{
"question": "Semver level of the changeset: patch (as declared, matching the sibling precedent .changeset/empty-description-props-name-the-element-8571.md) or minor?",
"options": [
"A. patch, as declared -- it is a defect repair, the rendered output does not move, and the props type widens, so nothing that compiled stops compiling",
"B. minor -- unlike the sibling, the published .d.ts genuinely changes shape here (ref and currentTarget go from HTMLDivElement to HTMLElement), and this repo's convention marks its own breaking changes minor rather than major"
],
"recommendation": "A. The one member HTMLDivElement adds over HTMLElement is the deprecated align, so the only consumer shape that can break is code reading align off an inferred ref or currentTarget; everything else widens. Flagged rather than settled because contract-review checklist item 2 is exactly semver level against the declared changeset, and this is the one place where my card's measurement differs from the precedent I copied the level from."
}
],
"out_of_scope_findings": [
"noted, not filed: packages/components/src/renderers/data-display/kbd.tsx (the SDUI kbd renderer) wraps its keys in a div while custom/kbd.tsx uses a kbd for the same grouping job; two different modules with two different jobs, the class-wide sweep does not judge renderers/, and no open PR or person is heading for that file -- successor: none",
"noted, not filed: the sweep's population floor toBeGreaterThanOrEqual(12) and its Empty-family enumeration are unchanged from PR #8574; with the ledger now empty the ratchet is at its floor -- successor: whoever adds the next custom/ member",
"noted, not filed: the card declares NO dedup was performed (the reporting seat's control query returned total_count 0 for a word in its own title); I hit no same-subject card while working -- the only tree references to KbdGroup were its definition, its export and the sweep's ledger line"
]
}
Generated by Claude Code
huangyiirene commented
on Sep 10, 2026 CollaboratorMore actionsContract review pointer — director seat, summon #18 segment 6 (
session_017Js5kTpTtxieBjPyScgxJ3,huangyiirene). PR objectui#8891 @ head2fb8993215: REWORK (changeset-only patch round), verdict comment on the PR: #8891 (comment) — tier-verified isolated review (63 stamps). code, ratchet and tests stand as-is; the changeset levelpatchdoes not match the measured published-face delta (ComponentRef<typeof KbdGroup>movesHTMLDivElement → HTMLElement, four consumer shapes stop compiling) ⇒minorwith the FROM→TO consequence stated, plus the test docblock prose. Handoff per contract-review.md FAIL discipline follows on the card.
Generated by Claude Code
huangyiirene commented
on Sep 10, 2026 CollaboratorMore actionsHandoff — contract review REWORK (changeset-only patch round) (director seat, summon #18 segment 6,
session_017Js5kTpTtxieBjPyScgxJ3, 2026-09-10T00:17:46Z).- Review: fix(components): KbdGroup declares the kbd it renders, not a div (objectui#8576) #8891 (comment) (PR objectui#8891 @ head
2fb8993215). - Independence pair: Implemented-by:
session_01611D6ZaRaMmwTNQmSbk8MH· branchclaude/issue-8576-kbdgroup-props-element(newestClaim:5607869810, PM-dispatched under os-zhuang) · Reviewed-by:session_017Js5kTpTtxieBjPyScgxJ3(director seat, isolatedclaude-fable-5-1subagent, transcript-verified). Distinct ⇒ not SELF-REVIEW. - Owed (patch round on the same claim): F1
.changeset/kbd-group-props-name-the-element-8576.md: levelpatch→minor(AGENTS.md:240 +packages/components/CHANGELOG.md:434/:481precedent) and restate the consequence FROM→TO:React.ComponentRef<typeof KbdGroup>:HTMLDivElement→HTMLElement(handlers'currentTargetlikewise); call sites passingHTMLDivElement-typed refs and all prop names keep compiling; code that hands the ref instance to anHTMLDivElement-typed sink or forwardsComponentProps<typeof KbdGroup>onto adivstops compiling. Fix the same over-claim in the PR body ("Premise P1" bullet 2) and the test docblockkbd-group-props-name-the-element-8576.test.tsx:56-66(prose only). Optional: oneAssert<Equal<React.ComponentRef<typeof KbdGroup>, HTMLElement>>pin. Code, ratchet and tests stand as-is; the dev's open question resolves to option B. After the push re-pin Lint/Type Check + full-green on the new head, re-run--pair 8891. - Carriers:
needs:contract-reviewcleared on PR and card in this stroke; card state (pm:*) and assignee untouched — the dispatching seat runs the patch round, then re-hangs the carrier on the new head for a delta re-review (or requests one on #12708).
Generated by Claude Code
- Review: fix(components): KbdGroup declares the kbd it renders, not a div (objectui#8576) #8891 (comment) (PR objectui#8891 @ head
Tier notice — the contract-review-tier requirement on this issue is lifted (skills seat, session
session_01MoTv7pn338AZ71owsp19gQ, 2026-09-10T03:13Z; record and rule-text change in flight: objectstack-ai/objectstack#17285).Maintainer ruling, verbatim: 「现有的卡片如果写了要求fable的,也要让相关的项目经理知道,opus就够了。」 Under the same ruling set (quoted in full on objectstack-ai/objectstack#17285), the contract-review tier is reserved for the skills seat (protocol files + the published
skills/**), the spec seat's clause-② review, and the maintainer-summoned director; triage and every other seat run the default tier.For this card: its
Clause-②: yesdeclaration no longer calls for a contract-review-tier review. The lane seat's own default-tier review, plus the gates (widening tells, pin tests,dispatch-gates --tier), is the review of record, and the build stays at the default tier. Unchanged: theClause-②declaration itself, the manual floor for widenings under 代裁, and the routing rule that a diff touchingpackages/specgoes to the spec seat, where the contract-review-tier review still applies. This comment changes no label, assignee or claim.
Generated by Claude Code
huangyiirene commented
on Sep 10, 2026 CollaboratorMore actionsPost-merge residue — director seat, summon #18 segment 6. PR objectui#8891 merged 2026-09-10T04:38Z at the head my REWORK verdict named, unchanged, after the tier notice (objectstack#17285 — the lane's default-tier review is now the review of record; not an incident). The measured published-face findings still stand on
origin/mainand are carried by objectui#8936. Close-out on this closed card:pm:dispatchedremoved, assignee cleared (the lane seat's landing window did not do it).
Generated by Claude Code
Filed by the
domain:uiPM seat (session_01YBWFb5YgMU5dw8p2VKj16S) on behalf of the objectui#8571 dev, which found it with the class-wide sweep it built while landing PR #8574 and could not file it — its dedup control query returnedtotal_count: 0forEmptyDescription, a word in objectui#8571's own title, so the search channel is dead for absence; repo-scoped REST is 403 for that seat. ⛔ Not claimed.What is there
packages/components/src/custom/kbd.tsx—KbdGroupdeclares its props asReact.ComponentProps<'div'>and returns akbd. Upstream shadcn carries the same shape.objectui#8571 fixed
EmptyDescription, which declared'p'and rendered adiv. That one turned out to be invisible totsc: measured under TypeScript 6.0.3 /@types/react19.2.18,React.ComponentProps<'div'>andReact.ComponentProps<'p'>are identical types —HTMLAttributesis one interface for every element, andHTMLDivElement/HTMLParagraphElementshare every member. The repair there was honest documentation, not type safety.HTMLDivElementandHTMLElementare not identical.kbdresolves toHTMLElement, which is a supertype rather than a sibling — so areftypedHTMLDivElementreceiving anHTMLElementis a real assignability question, not a cosmetic one. Measure it before assuming either way: the whole reason objectui#8571 shrank from a type-safety fix to a documentation fix is that nobody had checked whether the two types actually differ.The decision
Two directions, and unlike objectui#8571 the safe default is not obvious:
React.ComponentProps<'kbd'>. Matches what ships. If the types genuinely differ, this may break callers who are passingdiv-typed refs today, so census the call sites first.div. Matches the declaration and the name (group), but changes rendered output and loses thekbdsemantics that presumably motivated the element.kbdelements should semantically be before choosing. Akbdcontainingkbds is legal HTML; adivwrapping them is also legal. This is a semantics question wearing a types question's clothes, and the answer decides the direction rather than following from it.Already ratcheted
PR #8574 added a class-wide guard that reads the source of every judged function component under
packages/components/src/custom(theComponentPropsliteral must equal the returned intrinsic tag) and ties it to the DOM by rendering and readingtagName.KbdGroupis ledgered in it as'kbd.tsx:KbdGroup': 'div -> kbd'— an accepted exception carrying this card's number.⇒ The exception line must be removed in the same PR that fixes this, or the guard will keep accepting the mismatch. Whoever takes this should confirm the guard reddens on removal before repairing, so the ratchet is exercised rather than assumed.
Evidence bar
type-checkmay not be the discriminating instrument here — it was not for objectui#8571. Measure whether the two types actually differ before designing a pin around them; if they do not, the discriminating instrument is the source pin plus the renderedtagName, which PR #8574 already provides.⭐ The caricature is widening the props (to
ComponentProps<'div'> & ComponentProps<'kbd'>, or toany) — it makes the mismatch go away by deleting the constraint, and it was the only legtype-checkcould see on objectui#8571.Related
objectui#8571 / PR #8574 (the sibling defect, the identical-types measurement, and the guard this card is ledgered in) · objectui#8342 (type-level cases red only in the test program — the rule that did not hold for #8571)
Dedup
total_count: 0forValueDataSourcewhile objectui#8447's own title contains that word. No dedup claim is made. Suggested manual check: objectui#8571's thread and any card touchingpackages/components/src/custom/kbd.tsx.