Repository navigation
The three menu renderers run two different separator dialects against one MenuItem, and the declared one cannot parse green #6523
Description
Activity
os-support-ai commented
on Aug 26, 2026 CollaboratorAuthorMore actionsRouting note (PM,
domain:uilane) — evidence for triage, not a grading.This finding is adjacent to open
needs-user-decisioncard #6346: both concern the same shippedMenuIteminterface, one key over. #6346 asks a question about one member of that interface; this card reports that the interface as a whole is inconsistent with its three renderers in two independent ways (theshortcutruntime asymmetry, and the two contradictory separator dialects with a declared spelling that cannot parse green).Recording the relationship so whoever grades this one sees it. Not folding it — scope,
domain:*,typeand grading belong to the triage seat alone, and a decision card already in the maintainer's inbox is exactly the kind of thing an execution seat must not quietly enlarge.The practical consequence for triage: if #6346 is answered in a way that reshapes
MenuItem, this card's second finding is likely absorbed by that answer, while the first (theshortcutasymmetry, which corrects the premise of an earlier ruling — see PR #6525) stands on its own regardless.Also related: #6521 names the docs page that taught the wrong spellings, which is the upstream of the fixture defect this was found under.
Generated by Claude Code
- 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 Aug 26, 2026 os-support-ai commented
on Aug 26, 2026 CollaboratorAuthorMore actionsTriage (triage seat, hourly round 2026-08-26, session
session_01PVUQHLY6uhGUCs4mD9fzFC): first-touch →needs-user-decision, type Feature,domain:ui. This is a published-type contract fork (which separator dialectMenuItemdeclares, and whether the divider arm becomes representable) — human floor, not dispatchable. Premises verified onorigin/main: the two dialects sit exactly where the card says (menubar.tsx:33readsitem.separator;dropdown-menu.tsx:46/context-menu.tsx:44branch on the undeclareditem.type === 'separator'). The card itself asks for one ruling shared with #6346 (the handler-dialect key one over) — recommend adjudicating the two together. 决策四维分析如下(中文,决策卡通道):一句话问题:同一份菜单数据,三个菜单容器用两种互相矛盾的「分隔线」写法 —— 照官方类型写的作者在两个容器里得到一行空白菜单项,而真正能用的写法没有任何类型保护,随时会被改名改没。
选项 × 真实代价:
- A — 类型对、渲染器错:教 dropdown/context 两个容器读声明的
separator键并修正它们的注册表示例。业务上 = 三个容器终于说同一种话;代价:今天目录里 4 处、仓外未知数量的{type:'separator'}写法会失效(实测目录 4 处)。需 B 前置,否则分隔线在严格校验下根本写不出来。 - B — 让声明的分隔线可表达:把
MenuItem拆成「命令项(必有 label)| 分隔线(不能有 label)」的判别联合,或把 label 改可选。业务上 = 契约终于承认「分隔线」是一种真实存在的东西;判别联合更严,label 改可选会放松每个命令项的保护。 - C — 渲染器对、类型错:类型追认
type:'separator'写法、退役separator键。业务上 = 迁就现状,但 menubar 反过来成了孤儿,且给已发布类型加新键、废旧键,迁移义务最大。
四维(从业务立场):① 长远合理性:A+B 收敛到单一声明方言,联合类型把「分隔线没有标签」写进契约;C 是方言增生,让类型追着实现跑。② 实际业务拉动:实测存在 —— 目录 4 处在用未声明写法;照声明写的作者今天就在两个容器里撞见空白行(#6249 修掉的那个用户可见缺陷仍活在另两处);这不是投机面。③ 防 AI 犯错:判别联合 = 闭合枚举,AI 拼错写法会被 strict parse 响亮拒绝;今天 bare
z.object静默剥掉type还报成功(#6157 class-2),恰是 AI 批量犯错被掩盖的温床。④ 创业阶段不扩散:A+B 零新增公开键(联合是收紧);C 新增声明键并背上退役迁移。推荐:A+B(与 #6346 同批一裁,一个答案不是两个);回退:C,仅当裁前实测发现仓外
{type:'separator'}存量大到迁移不起。置信缺口:仓外/客户应用里两种写法的真实存量未测(卡已声明,裁前值得测一次);shortcut键 menubar 是否补渲染(两容器已有运行时,menubar 补上是对齐而非新能力)在本卡只是记录,未单独立卡,随本裁决一并定向即可。
Generated by Claude Code
- A — 类型对、渲染器错:教 dropdown/context 两个容器读声明的
os-support-ai commented
on Aug 26, 2026 CollaboratorAuthorMore actionsos-decision-facets
Marker repair (triage seat, sessionsession_01PVUQHLY6uhGUCs4mD9fzFC): the HTML-comment spelling of the facets marker was stripped by the sanitizer from the four-facet analysis comment above (posted 09:16Z, same session — the analysis text itself survived intact). This comment carries the marker in literal-text spelling so the machine grep finds this card's facets block; the analysis lives in the 09:16Z comment.
Generated by Claude Code
Maintainer ruling recorded — A+B: one separator dialect (the declared one), and the divider becomes representable
Provenance: maintainer, 2026-08-27, PM chat decision-inbox batch 5 (adjudication session
session_01DKWDdUJ2XNRESVVWUvcpnh), verbatim: 「同意」 — accepting the triage recommendation A+B (2026-08-26T09:16Z facet block), ruled in one sitting with #6346: one answer for the wholeMenuItemfamily.Ruled:
- B first (precondition):
MenuItembecomes a discriminated union — a command item (label required) | a divider (separator: true, no label). The union, not label-optional: it is what the data actually is, and it keeps every command item's label protection. Strict parse of the declared divider spelling goes green for the first time; the counter-probe incomponent-fixture-declared-keys.test.tsflips to a real parse pin. - A on top:
dropdown-menuandcontext-menubranch on the declareditem.separator; theirdefaultPropsand registrydescriptionstrings stop teaching the undeclared{ type: 'separator' }; the 4 catalog fixtures migrate. - Rider, recorded as parity not new capability: menubar renders
shortcut— the declared key already has working runtime in the other two containers, so this aligns the third rather than expanding the surface (this corrects the premise of the earlier menubar-scoped reading, as this card documented).
⛔ C (type follows the renderers) declined — dialect accretion, and it would orphan menubar instead. Fallback pre-recorded: if implementation surfaces a significant external
{ type: 'separator' }corpus (unmeasurable from here), stop and report — C becomes the reconsideration, not a silent widening.Dispatch notes: Clause-②: yes — a published type reshapes (union) and renderer accept behaviour changes; contract-review tier. Family dispatch with #6346 permitted under the five fold gates; each card keeps its own
Fixesand pins (divider absent-on-declared-spelling red-before-fix in both containers; blank-row regression pin).State:
needs-user-decision→pm:queue.
Generated by Claude Code
- B first (precondition):
Claim:
domain:ui@ objectui PM seat, session8ca04858-ea8e-5b85-9182-de59aa49e00c— branchclaude/issue-6523-menuitem-separator-family.⭐ Dispatched as a FAMILY with #6346, one dev, one branch — because the ruling says to. Quoting it: "Family dispatch with #6346 permitted under the five fold gates; each card keeps its own
Fixesand pins." The maintainer ruled both in one sitting on 2026-08-27 (「同意」), explicitly as "one answer for the wholeMenuItemfamily."This corrects a note this seat had been carrying: I had #6523 and #6346 down as twins on one file face that could only run serially, with one queued behind the other. That was a reading of the file overlap, not of the ruling — and the ruling had already answered it in the better direction. ⭐ The lane rule earns another instance: a constraint you inferred from the code is not a constraint if a ruling already addressed it. Three cards this round (#6458, #6614, and now this pair) turned on reading the thread to the end.
Dispatching on the ruling as written, in its stated order:
- B is the precondition:
MenuItembecomes a discriminated union — command item (label required) | divider (separator: true, no label). ⛔ The union, not label-optional: the ruling chose it because it is what the data actually is and it keeps every command item's label protection. Consequence to verify: a strict parse of the declared divider spelling goes green for the first time, and the counter-probe inexamples/schema-catalog/test/component-fixture-declared-keys.test.tsflips from a structural assertion to a real parse pin. - A on top:
dropdown-menuandcontext-menubranch on the declareditem.separator; theirdefaultPropsand registrydescriptionstrings stop teaching{ type: 'separator' }; the 4 catalog fixtures migrate. - Rider, and its framing is load-bearing: menubar renders
shortcut— recorded as parity, not new capability, because the declared key already has working runtime in the other two containers. This card is what corrected the earlier menubar-scoped premise; ⛔ do not re-derive it as a capability expansion.
⛔ C declined (dialect accretion, and it would orphan menubar). ⭐ But the ruling pre-recorded its own fallback: if implementation surfaces a significant external
{ type: 'separator' }corpus, stop and report — C becomes the reconsideration, ⛔ never a silent widening. That is a real stop condition, not a formality: the confidence gap the triage facets named is exactly the out-of-repo prevalence of the undeclared spelling.Clause-② yes — a published type reshapes and renderer accept behaviour changes. Contract-review tier at review; ⛔ no
needs:contract-reviewlabel (phantom here, zero readers). Changeset scoresminorwith the break written out, percheck-changeset-no-major.mjsand today's maintainer confirmation.Each card keeps its own
Fixesand its own pins, per the ruling.
Generated by Claude Code
- B is the precondition:
- added a commit that references this issue
on Aug 28, 2026 os-dev-report
{ "issue": 6523, "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, covering both #6523 and #6346 on one branch as the ruling authorised, each keeping its ownFixesand pins. Flipped ready; auto-merge once its table is green.Landed as ruled, in the ruled order:
- B first:
MenuItemis now a discriminated union —MenuCommandItem(label required) |MenuDividerItem(separator: true, no label). ⭐ The union, not label-optional, so every command item keeps its label protection. - A on top:
dropdown-menuandcontext-menubranch on the declareditem.separator, matchingmenubar; theirdefaultPropsand registrydescriptionstrings stop teaching{ type: 'separator' }. - The rider:
menubarrendersshortcut— parity, not new capability. - The menu renderers read an undeclared
item.onSelect, while the declaredMenuItem.onClickis read by nothing #6346's half: all three renderers now fire the declareditem.onClick.⚠️ Worth stating plainly because it is worse than the card implied —dropdown-menuandcontext-menuwere reading the undeclareditem.onSelect, andmenubarwired no handler at all.renderMenuItems/renderContextMenuItemstighten fromany[]toMenuItem[].
⭐
typeis TOMBSTONED, not deleted — the distinction that made this fix realtypeisneveron both arms. That is the whole reason this card existed:MenuItemSchemawas a barez.object, so the undeclared key was silently stripped while the parse still reported success. A deletion would have left exactly that behaviour in place — the key stops being judged and keeps being accepted. A declared refusal is what makes the retired dialect fail loudly.⭐⭐ And the tombstone immediately earned its keep: 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 anywhere. Both removed and documented as forced consequential cleanups rather than slipped in. Neither would have surfaced from a deletion; only declaring the refusal made the compiler point at them.The stop condition was checked, not assumed
The ruling pre-recorded it: a significant external
{ type: 'separator' }corpus ⇒ stop, and C becomes the reconsideration. Checked — no external corpus measurable from here, not triggered. And the in-repo count was re-derived, not inherited: 4 sites, which decompose as 2 catalog fixtures + 2 registrydefaultProps, not the "4 catalog fixtures" the card's wording suggested. Same total, different composition — the kind of thing that matters when someone later greps for fixtures and finds two.⭐⭐ Leg B: a miss that was root-caused instead of accepted
Leg A (revert the divider-dialect fix): predicted 6 of 8 red with 2 green (menubar untouched) — observed exactly that.
Leg B (revert the
onClickwiring): predicted 6/6 red; first run gave 5/6. The dev did not accept 5/6 as close enough, and did not report a renderer defect. It root-caused the gap to a bug in its own ablation harness's regex, fixed the harness, and re-ran to a full 6/6.⇒ That is the correct handling of a near-miss in both directions: a wrong instrument was not allowed to masquerade as either a passing prediction or a finding about the code. Third time this week a dev has chased its own instrument to ground rather than the code — it keeps paying.
⚠️ check-doc-snippet-types.mjsreported NOT MEASURED (PRECONDITION NOT MET, unrelated unbuilt packages) and was reported in those words, with the note that this diff touches only plaintext fences that gate does not cover. ⛔ Neither green nor red — correct.⭐ One pre-existing, unrelated defect found while implementing —
core/app-schema.mdx's Global Actions snippet imports the wrong same-namedMenuItem(resolving to overlay's through the@object-ui/typesbarrel instead ofapp.ts's ownAppMenuItem) — was filed as #6692 rather than fixed. Correct: it belongs to neither ruling, and it is its own name-collision class.
Generated by Claude Code
- B first:
- added a commit that references this issue
on Sep 1, 2026 - added 2 commits that reference this issue
on Sep 1, 2026
Found while implementing #6249 (repairing the menubar demo's separators to the declared
separator: truespelling). Filed unassigned and out of that card's fence — #6249 is scoped to one fixture, and this is a code-side contract question whose fix lands inpackages/.This is the fourth
MenuItem-shaped defect in the family alongside #6326 and #6278 (the item'sicon) and #6346 (the item's handler). Unlike those it is about the divider and the shortcut.What was measured, at
50f987f9aMenuItemis one interface, shared bydropdown-menu,context-menuandmenubaritems. The three renderers do not agree on how to read it.1. Two separator dialects, and only one of them is declared.
MenuItemdeclaresseparator?: boolean(packages/types/src/overlay.ts:358,zod/overlay.zod.ts:139) and declares notypeat all.MenuItemSchemais a barez.object, sotypeis silently stripped and the parse still reports success — which is why no gate has ever reported this.Both sibling renderers also teach the undeclared spelling in their own registry metadata:
dropdown-menu.tsx:124andcontext-menu.tsx:115write{ type: 'separator' }indefaultProps, and both registrydescriptionstrings document{ type?: "separator"|"label", ... }.menubar.tsx:69writes the declared{ separator: true }. The registry itself ships both dialects.2.
shortcutis rendered by two of the three.menubar.tsxreads it nowhere.⭐ This matters to a ruling already made. #6249's triage ruled that widening
shortcutand teaching the renderer to draw it would be "a capability expansion with zero runtime today". That is exactly true for menubar, which is what the card measured — but it is not true of theMenuItemsurface: the declaredstringspelling already has working runtime in two of the three containers. So "menubar rendersshortcut" would be parity with shipped runtime, not a new capability. That does not reopen #6249 (which correctly removed an affordance its renderer cannot draw), but it changes the input to any future ruling and should not be rediscovered from scratch.3. The declared separator spelling cannot parse green.
MenuItem.labelis required —label: string(overlay.ts:334),z.string()(overlay.zod.ts:133) — and a divider has no label. Measured:menubar.tsx:69's owndefaultPropswrite{ separator: true }with no label, so the shipped renderer's own default value does not satisfy the shipped type. AMenuItemis really a union — a command item (needs a label) or a divider (cannot have one) — declared as a single object where the divider arm is unrepresentable.This is not theoretical: it directly shaped #6249. The corrected fixture uses the declared spelling and therefore cannot be pinned by a
MenubarSchema.safeParse, so the assertion there had to be structural. That is recorded in a counter-probe inexamples/schema-catalog/test/component-fixture-declared-keys.test.tsso the next reader does not "simplify" it into a parse that would silently measure nothing.Why it matters
Declared and enforced disagree in both directions on the same object, the failure mode #6132's four-axis analysis called the most expensive ("能通过校验、能发布、然后静默什么都不做"):
{ separator: true }in a dropdown menu — it validates, it publishes, and it renders a blank menu row. That is the exact user-visible defect finding(examples): the live menubar demo teaches ashortcutarray against astringslot, and its separator renders as a blank menu item #6249 was filed for, still live in the other two containers for anyone who follows the declaration;{ type: 'separator' }— which no type declares, so nothing protects it from being renamed away;The decision this needs
A — the type is right, the renderers are wrong. Teach
dropdown-menuandcontext-menuto branch onitem.separator, and correct theirdefaultPropsand registry descriptions. Requires B as a precondition (a divider must be expressible), and breaks every authored{ type: 'separator' }— 4 in the catalog today, unknown in the wild.B — make the declared divider representable. Either
label?: string, or splitMenuIteminto a discriminated union ({ separator: true }|{ label: string, ... }). The union is stricter and is what the data actually is; makinglabeloptional is one line but weakens every command item.C — the renderers are right, the type is wrong. Declare
type?: 'separator' | 'label'onMenuItemand retireseparator. This moves a published type and would makemenubar.tsxthe odd one out instead.I have no recommendation strong enough to skip triage. A+B matches the declaration and AGENTS.md #0.1 (fix the producer, never accrete renderer-side dialects), and is what I would lean to; C matches observed runtime in two of three renderers. The choice turns on whether any authored
{ type: 'separator' }exists outside this repo, which is worth measuring before ruling. Note the same question was asked one key over in #6346 and is still open — these two probably want one answer, not two.Refs: #6249 · #6346 · #6326 · #6278 · #6132 · #5250.
Generated by Claude Code