Repository navigation
The menu renderers read an undeclared item.onSelect, while the declared MenuItem.onClick is read by nothing #6346
Description
Activity
进决策箱 —— 并补上作者说「裁决前值得先测」的那个读数
由
domain:devx@ objectui 执行席(#5748)记录,PM 会话session_012CZgmFFzqA9cX8tBMhvpFe,R30。⛔ 我不设domain:*—— 路由归分诊席。挂needs-user-decision的理由在最后一节:这是 PR #6345 的落地闸门,不只是一张后续卡。先复核事实(我自己在
origin/main上跑的,不是转述)读数 结果 MenuItem.onClick是否声明✅ packages/types/src/overlay.ts:346三个菜单渲染器是否读 onClick⛔ 零命中 它们读什么 item.onSelect——context-menu.tsx:65、dropdown-menu.tsx:67;menubar.tsx两个都不读探针可信,因为同一条 grep 在同样的文件里命中了
onSelect—— 零命中是读数,不是漏查。⭐ 作者点名「取决于野外有没有 authored
onSelect」—— 测了,是 0git grep -rn '"onSelect"' origin/main -- examples content/docs apps → 0 命中对照(证明我扫的地方不是空的):同一批目录里确实存在菜单 schema ——
components-overlay-context-menu/basic-context-menu.json、components-overlay-dropdown-menu/{basic,with-icons}.json、components-overlay-menubar/application-menubar.json。所以「0」是「这些菜单 schema 一个都没写onSelect」,不是「没扫到菜单」。⚠️ 边界要说清:这测的是本仓。仓外消费者无法从类型学到onSelect(它没被声明),只能靠读渲染器源码,所以外部使用不太可能 —— 但不是不可能,而我测不到。四维分析
① 实际业务需求. 选项 A 唯一的成本是「已写
onSelect的作者会失效」,而这个成本在本仓实测为 0。反过来,今天真实存在的伤害是:一个照着已发布类型写onClick的作者,校验通过、发布通过、然后什么都不发生。前者是假想成本,后者是现成的。② 项目长远合理性. 北极星是 declared = enforced。A 让实现向已发布契约收敛;B 让契约向实现让步,并且移动一个已发布类型(
MenuItem.onClick已经在 dist 里、在 zod 镜像里)。契约跟着实现跑,下次实现再漂,契约再跟一次 —— 那不是收敛。③ 防 AI 写代码犯错. 这一轴最硬,而且指向 A。AI 写元数据 app 时读的就是类型。
onClick声明在那里,AI 会用它,拿到一个静默无效的菜单 —— 卡片自己的话:「能通过校验、能发布、然后静默什么都不做」。B 修好了这一点,但代价是把 AI 从没读过的那个拼写扶正,同时让今天所有照类型写的人失效。A 是唯一让「读类型 → 能用」成立的选项。④ 创业阶段不扩散需求. A 改三个渲染器 + 一个 helper 的类型(
renderMenuItems(items: any[])→MenuItem[]),不动已发布面,不新增能力。B 要动已发布类型 + zod 镜像 + 刚由 PR #6345 落的三个文档页。A 明显更省。C 已被卡片正文以 #4453/#5329/#6107 三次先例否掉,我同意。⭐ 顺带:把 helper 从
any[]收成MenuItem[]本身就是这个 bug 能存在的原因 —— 它是让「声明与实现不一致」得以 type-check 通过的那道口子。A 顺手关掉它。PM 推荐:A
理由压在 ①③ 上:A 的迁移成本在本仓实测为零,而它是唯一让「照已发布类型写」这件事真正работа的选项。⛔ 推荐是输入不是放行。
⚠️ 若你选 B,连带项已经点名:PR #6345 刚把三个文档页指向onClick,B 之下它们要跟着改回来 —— 这是把一张已合并的文档 PR 再翻一次,成本要算进 B。⛔ 这张卡是 PR #6345 的落地闸门
PR #6345 照裁决字面执行是对的(裁决的判据是「declared」,
onClick满足它;它点名要防的「指向第二个未声明的 prop」也没有发生)。但实施中测出了对向事实:那个重定向目标声明存在、运行时无人读取。按本席技能对这种情形的规定 —— 照字面执行、冲突立成决策卡、该 PR 不挂 auto-merge、留异议窗口 —— PR #6345 保持 draft、未武装,等你在这里落一句。Refs: PR #6345 · #6132(裁决所在)· #6326 / #6278(同族的
MenuItem缺陷)
Generated by Claude Code
Maintainer ruling recorded — Option A: the type is the contract; all three renderers read
onClickProvenance: maintainer, 2026-08-27, PM chat decision-inbox batch 5 (adjudication session
session_01DKWDdUJ2XNRESVVWUvcpnh), verbatim: 「同意」 — accepting recommendation A (seat 2026-08-25T14:37Z analysis + triage facet block), ruled in one sitting with #6523 as both cards asked: one answer for the wholeMenuItemfamily.Ruled: all three menu renderers read the declared
item.onClick(dropdown/context switch off the undeclaredonSelect; menubar — which currently wires no handler at all — gains the wiring);renderMenuItemstightens fromany[]toMenuItem[], closing the hole that let the divergence type-check. Migration cost measured zero in-repo (authoredonSelect: 0 hits with firing positive controls). ⛔ C (both spellings, one deprecated) stays refused per the three standing precedents.Consequences released by this ruling: PR #6345 (the three doc pages naming
onClick) is un-gated — the redirect target is confirmed the contract; the PR proceeds on its normal path. Tripwire: if implementation surfaces external authoredonSelectcorpus, stop and report — do not widen to dual-dialect silently.Dispatch notes: family dispatch with #6523 permitted if the five fold gates pass (same interface, same renderer set, both ruled, per-card pins, exclusion list stated); Clause-② rides the #6523 half (the union type change) — this half changes renderer reads to match the published type.
State:
needs-user-decision→pm:queue.
Generated by Claude Code
Claim:
domain:ui@ objectui PM seat, session8ca04858-ea8e-5b85-9182-de59aa49e00c— branchclaude/issue-6523-menuitem-separator-family.⭐ Dispatched as a family with #6523, one dev, one branch, on the maintainer's instruction. Both cards were ruled in a single sitting on 2026-08-27 as "one answer for the whole
MenuItemfamily", and #6523's ruling states: "Family dispatch with #6346 permitted under the five fold gates; each card keeps its ownFixesand pins."So this card is not queued behind #6523 — they land together. ⛔ But they are not merged into one deliverable either: this card keeps its own
Fixes #6346and its own pins. The family is a dispatch unit, not a scope merge.The dispatch order sends the dev to read this card's own ruling rather than inferring it from #6523's — the two decide different keys of the same interface (this one the handler dialect, #6523 the divider and the shortcut), and a ruling read second-hand is a ruling half-read.
The load-bearing shared fact both halves rest on:
MenuItemSchemais a barez.object, so an undeclared key is silently stripped and the parse still reports success — which is why no gate has ever reported either dialect. That is the same "declared ≠ enforced, and nothing notices" shape this lane keeps meeting, and it is why the fix has to be a declared refusal rather than a renderer-side tolerance.Clause-② yes, contract-review tier; changeset
minorwith the break spelled out.
Generated by Claude Code
- added a commit that references this issue
on Aug 28, 2026 os-dev-report
{ "issue": 6346, "issues": [ 6523, 6346 ], "status": "done", "branch": "claude/issue-6523-menuitem-separator-family", "pr": "https://github.com/objectstack-ai/objectui/pull/6693", "premise_still_valid": true, "summary": "Family dispatch, one branch/PR for both #6523 and #6346 per the ruling. MenuItem (overlay.ts, shared by dropdown-menu/context-menu/menubar) is now a discriminated union MenuCommandItem | MenuDividerItem: label required on the command arm, separator:true alone on the divider arm, type tombstoned (never) on both. dropdown-menu and context-menu now branch on the declared item.separator (matching menubar) instead of the undeclared item.type, and their registry defaultProps/description strings stop teaching the retired dialect; the 2 catalog fixtures + 2 registry defaultProps (4 total, re-derived) migrate. All three renderers now fire the declared item.onClick (dropdown-menu/context-menu used to read undeclared item.onSelect; menubar wired no handler at all); renderMenuItems/renderContextMenuItems tighten from any[] to MenuItem[]. menubar also renders shortcut (parity rider). Stop condition checked: no external type:separator corpus measurable from here, none triggered. Two forced consequential cleanups documented in the PR body: item.type === 'label' (a third undeclared dialect on the same now-tombstoned key) removed as dead code, and item.inset (undeclared, never authored anywhere) dropped. Docs (three .mdx Schema blocks) corrected for exactly this diff; a pre-existing unrelated docs/type-naming collision found while implementing was filed as #6692 rather than fixed (see out_of_scope_findings).", "tests": "pnpm --filter @object-ui/types type-check: PASS (tsc --noEmit && tsconfig.examples.json && tsconfig.test.json, exit 0). pnpm --filter @object-ui/components type-check: PASS (tsc --noEmit && tsconfig.test.json, exit 0). pnpm exec vitest run packages/types/: 69 files / 828 tests PASS. pnpm exec vitest run packages/components/: 202 files / 1837 tests PASS. pnpm exec vitest run examples/schema-catalog/test/component-fixture-declared-keys.test.ts: 33 tests PASS. node scripts/check-control-bytes.mjs, check-doc-component-types.mjs, check-doc-fence-languages.mjs, check-lucide-icon-record-names.mjs, check-changeset-presence.mjs, check-changeset-no-major.mjs: all PASS. eslint on all touched files: 0 errors (warnings only, all pre-existing patterns). check-doc-snippet-types.mjs: NOT MEASURED, PRECONDITION NOT MET (16 unrelated packages unbuilt in this fresh worktree; gate only compiles ts/tsx fences and every block this PR touches is plaintext, unchanged, so this diff cannot be implicated). Ablation, both legs predicted before running, mutation+restoration proven on disk (anchored grep -c counts before/after, git diff HEAD empty after git checkout HEAD -- PATH restore): Leg A (revert divider-dialect fix only) -> predicted 6 of 8 divider/blank-row tests red, 2 menubar-shortcut tests green (menubar untouched) -> observed exactly that, 6 failed/2 passed. Leg B (revert onClick-wiring fix only) -> predicted all 6 onClick-handler tests red -> first attempt got 5 failed/1 passed because the menubar submenu-child mutation regex missed a 3-line comment (an ablation-harness script bug, not a renderer finding); corrected and re-run to the full predicted 6/6 red, anchored counts confirming 0 remaining wiring occurrences. All gates re-confirmed green on the restored tree at committed HEAD c75b928e6 after both legs.", "mcp_calls": "14", "open_questions": [], "out_of_scope_findings": [ "filed as #6692: core/app-schema.mdx's Global Actions ts snippet imports the wrong same-named MenuItem (resolves to overlay's via the @object-ui/types barrel instead of app.ts's own AppMenuItem) for typing AppAction.items -- pre-existing, unrelated to either ruling, doesn't break under this diff (bare type reference, not an object literal)", "not filed, noted in PR body only: open issue #6521 (pm:blocked, unassigned) already covers the value/variant fields these three docs pages invent and MenubarSchema.menus's taught-as-required optionality -- left untouched here as #6521's scope; this PR's menubar.mdx edit follows #6521's own guidance to 'align with whichever way #6523 is ruled'" ] }
Generated by Claude Code
ACCEPT — PR #6693, head
c75b928e6, landed with #6523 on one branch as the ruling authorised. This card keeps its ownFixes #6346and its own pins; the family was a dispatch unit, not a scope merge.What the measurement found was worse than the card said
The card reported that the menu renderers read an undeclared
item.onSelectwhile the declaredMenuItem.onClickis read by nothing. Confirmed — and the third container was worse:dropdown-menuandcontext-menuread the undeclareditem.onSelect;⚠️ menubarwired no handler at all.
So the declared key had zero live readers across all three, and one container had no click path whatever. All three now fire the declared
item.onClick, andrenderMenuItems/renderContextMenuItemstighten fromany[]toMenuItem[]— which is what stops the next undeclared handler spelling from being reachable in the first place.⭐ Why this could only be fixed together with #6523
Both halves rest on one mechanism:
MenuItemSchemawas a barez.object, so an undeclared key is silently stripped and the parse still reports success. That is why neither dialect — the divider's nor the handler's — was ever visible to a gate. Fixing one key while the other stayed unjudged would have left the mechanism intact.The ruling's "one answer for the whole
MenuItemfamily" was therefore the right shape, and the union plus thetypetombstone is what removes the silence for both.⭐ Evidence that the tombstone does real work: it forced out a third undeclared dialect on the same key —
item.type === 'label', dead code nobody had named — plusitem.inset, undeclared and never authored. Both documented as forced consequential cleanups rather than slipped in.The ablation on this card's half
Leg B reverted the
onClickwiring: predicted 6/6 red, first run gave 5/6. ⭐ The dev root-caused the gap to a bug in its own ablation harness's regex — ⛔ not a renderer defect — fixed the harness and re-ran to a full 6/6. A wrong instrument was not allowed to pass as either a satisfied prediction or a finding about the code.
Generated by Claude Code
- added a commit that references this issue
on Sep 1, 2026
Surfaced while implementing objectui#6132 (PR #6345). Not fixed there: that card ruled a docs question, and this is a code-side contract question whose fix lands in
packages/.What was measured
objectui#6132's ruling redirects three menu reference pages at
MenuItem.onClick, on the grounds that it is a real declared slot. It is — I verified it in three places before doing the redirect:packages/types/src/overlay.ts:346—onClick?: () => void;packages/types/dist/overlay.d.ts:334— the exact line objectui#6132 citedpackages/types/src/zod/overlay.zod.ts:136—onClick: z.function().optional().describe('Click handler')But no renderer reads it. The renderers read a different, undeclared key on the same item:
packages/components/src/renderers/overlay/menubar.tsxwires no item handler at all — neither spelling.Neither
onSelectnorinsetis declared onMenuItem. The helper is typedrenderMenuItems(items: any[]), which is why the mismatch type-checks.Confirming the negative:
Why it matters
Declared and enforced disagree in both directions on the same object:
onClick— it validates, it publishes, and it never fires;onSelect— which no type declares, so nothing protects it from being renamed away.This is the failure mode objectui#6132's own four-axis analysis called the most expensive one ("能通过校验、能发布、然后静默什么都不做"), except here it survives because the prop is declared, so it reads as correct.
Note this is a third
MenuItem-shaped defect in the same family as objectui#6326 and objectui#6278 (both about an item'sicon), and unlike those it is about the handler rather than the glyph.The decision this needs
Which spelling is the contract?
item.onClick, type the helper asMenuItem[]. Existing authoredonSelectkeys stop working.MenuItem.onClicktoonSelectin TS + Zod, and correct the three doc pages PR docs(components): follow the shipped types on the last five action props #6345 just pointed atonClick. This moves a published type.ActionConfig: it makes the docs retroactively true by widening the surface, and objectui#4453/Migrate the in-repo schema corpus (~424 files) to carry the canonical$schemamarker — the producer-side half of #5127's Option B #5329/Four fields docs pages import a cell renderer @object-ui/fields does not export — and two of them name a renderer that has no export at all #6107 have refused that three times.I have no recommendation strong enough to skip triage — B matches observed runtime behaviour, A matches the published declaration, and the choice depends on whether any authored
onSelectexists in the wild. Worth measuring before ruling.overlay/context-menu.mdx,overlay/dropdown-menu.mdx,overlay/menubar.mdx) nameonClickand would need the same correction under option B.Refs: objectui#6132 · PR #6345 · objectui#6326 · objectui#6278 · objectui#4453.
Generated by Claude Code