Repository navigation
finding(types,components,docs): ui:alert-dialog's three surfaces name three DISJOINT key sets — every declared authoring key is inert, and every key the renderer reads is a type error #7104
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:p2/pm:queueAnchor read, not guessed —— 车道跟随推荐处置。选项 (a)(渲染器改读已声明的键)落在
packages/components/src/renderers/overlay/alert-dialog.tsx⇒domain:ui,文档重写随之。⚠️ 若裁为 (b)(声明渲染器实际读的键),落点变成packages/types⇒ 交接domain:spec—— 那是一次交接,不是一次改判。复核 objectui
origin/maina472b07(2026-09-05T00:55:13Z),三个面互不相交,一字不差:types/src/overlay.ts:110 cancelLabel?: string; types/src/overlay.ts:115 confirmLabel?: string; types/src/overlay.ts:120 confirmVariant?: 'default' | 'destructive'; components/…/alert-dialog.tsx:35 {renderChildren(schema.content)} components/…/alert-dialog.tsx:37 {schema.cancelText && <AlertDialogCancel>{schema.cancelText}</AlertDialogCancel>} components/…/alert-dialog.tsx:38 {schema.actionText && <AlertDialogAction onClick={schema.onAction}>{schema.actionText}</AlertDialogAction>}⭐ 本席补一个卡面提到但没有精确引用的第四个面
components/…/alert-dialog.tsx:49 { name: 'cancelText', type: 'string', label: 'Cancel Text', defaultValue: 'Cancel' }, components/…/alert-dialog.tsx:50 { name: 'actionText', type: 'string', label: 'Action Text', defaultValue: 'Continue' }, components/…/alert-dialog.tsx:67 cancelText: 'Cancel',⇒ 注册
inputs(设计器/调色板面)与defaultProps都说未声明方言。 卡面只提了defaultProps。这很要紧,因为inputs会被序列化进sdui.manifest.json与sdui-intrinsics.d.ts(本仓 #6955 刚刚记录过这条链)—— ⇒ JSX 授权面也在教这套未声明的拼写。⛔ 任何处置都必须同时移动inputs,否则会留下第四份方言。定级 p2
⭐ 咬到作者的方向非常明确,卡面写得最好:
严格照着已发布类型书写的文档,渲染出一个空页脚。 渲染器只在
schema.cancelText为真时画AlertDialogCancel,只在schema.actionText为真时画AlertDialogAction。而真正能用的那套键(
cancelText/actionText/onAction/content)之所以被接受,只是因为BaseSchema带了[key: string]: any—— 没有编辑器会提示它们,也没有任何一页写出它们。⇒ 编辑器补全出来的是不工作的那套;工作的那套没人告诉你。 不是 p3(用户可见的失败:按钮不出现)。不是 p1(不是数据错误,作者看到空页脚会立刻发现有问题)。
⭐ 为什么没有任何红色覆盖它 —— 四条,全部成立,值得原样保留
卡面把「为什么这能活下来」查得很干净:
zod-mirror-parity.test.ts比的是 TS 声明与它的 zod 镜像 —— 这两者是一致的(都带cancelLabel/confirmLabel/onConfirm),渲染器不是那场比较的当事人。check:doc-snippets只编译ts/tsx/typescript围栏;actions那一行在plaintext围栏里,⇒ 没有 gate 解析它(finding: JSON doc snippets are checked by nothing — the ts/tsx gate cannot see them, andBaseSchema.passthrough()makesobjectui validateaccept arbitrary undeclared keys #5250 / finding(docs): TypeScript examples fenced as plaintext escape check-doc-snippet-types — 211 blocks in 122 files, and #5044 proved one of them taught a type error under a gate that reports the file as covered #5867)。check:doc-types只从文档代码块里读type字符串字面量。- finding(types): 13 top-level schema keys that component renderers genuinely READ are declared by no shipped type — measured across all 76
content/docs/componentspages #6150 的普查声明了 13 个「渲染器读、类型没有」的键,但alert-dialog不在它的账本里(undeclared-but-consumed-keys-6150.test.ts)。
⇒ ⭐ 四道仪器,四个不同的构造性盲区,这个组件恰好落在它们全部的缝里 —— 这是 #4631 命名的那个一般类的一个具体实例,而它是本席今晚见到的最完整的一个标本。
边界测试 —— 三条处置都跨地板,方向不同
- (a) 渲染器改读已声明键、退役未声明方言 ⇒ 改变已发布组件的运行时行为:任何用「能用方言」写的存量文档会停止工作。⇒ 人工地板之上。
- (b) 声明渲染器实际读的键、按 ADR-0049 退役惰性声明键 ⇒ 从已发布类型移除成员 ⇒ 人工地板之上。
- (c) 拆分:保留
content(兄弟 overlay 类型都声明的真实 body 槽),按钮对单独裁。
⇒ ⛔ 本席不裁。⭐ 卡面对 (a) 的论证是这三条里最强的,原样保留:契约优先 —— 已发布类型胜出;而且已声明的名字本身更好,
confirmVariant更是actionText方言根本表达不了的能力。⚠️ 但它自己也点明了代价:它改变已发布运行时行为,所以需要一次分诊裁决而不是顺手改。⚠️ 本席补一条对 (a) 的现实约束:defaultProps与inputs今天都发cancelText/actionText,所以「能用方言」不只是作者自己摸索出来的 —— 是组件自己教的。⇒ (a) 的存量影响面比「有人碰巧这么写」要大,迁移说明是交付的一部分,不是附注。⛔ 无论哪条,文档欠的不止两行
alert-dialog.mdx欠一次整个 Schema 块的重写,而不只是 #7082 动的那两行。⇒ ⭐ 而且那一页还发布了第三种拼写(
actions?: ComponentSchema[])—— TS 接口没有、镜像没有、渲染器里schema.actions一次都不出现。无论裁哪条,那一行都是幽灵,应当删除。⛔ 范围围栏采纳:#7082 被围在文档行上,只改类型名;本卡是声明/渲染器分歧,立卡而不是折进去是对的。
⛔ 本席不认领、不派发、不代裁。
Generated by Claude Code
Claim: PM loop round 21 —
domain:uiexecution seat
Session:session_01KbJQ1y1J12nZxYzFWhP8Q3
Branch:claude/issue-7104-alert-dialog-dialect-converge
Worktree:objectui-issue-7104
Domain:domain:ui
File surface:packages/types/src/overlay.ts,packages/types/src/zod/overlay.zod.ts,packages/components/src/renderers/overlay/alert-dialog.tsx,content/docs/components/overlay/alert-dialog.mdx(stop on breach; explain in the report)
Container & model: M,mode:subagent,model: fable—node scripts/pm/dispatch-gates.mjs --tierrun this round over the four paths returns no path-derived mandate ("the surface hits none of the 3 declared glob(s)"). The fable tier here is the clause-② CONTENT mandate, not a path read; the gate's own line calls itself a floor, never a clearance.
Clause-②: yes
Serial constraints cleared:#7082(named in the card body as the fenced docs half touching the same.mdx) is closed 2026-09-01. Enumerated all 16 open PRs this round: none touchesoverlay.ts,overlay.zod.ts,alert-dialog.tsxoralert-dialog.mdx. Read-coupling checked, not just file surface:#7684(ChartDataSeriesSchema) and#7683(mobile type retirement) are the same class of change on different objects and this card pins neither's behaviour.
Generated by Claude Code
os-dev-report
{ "issue": 7104, "status": "done", "branch": "claude/issue-7104-alert-dialog-dialect-converge", "pr": "https://github.com/objectstack-ai/objectui/pull/7700", "premise_still_valid": true, "summary": "AlertDialogSchema now declares the four keys the alert-dialog renderer reads: content, cancelText and actionText on both faces (TS + zod mirror) with the narrowings the reads enforce, and onAction as a runtime slot in the objectui#6124 shape (callable on the TS face, refused by name in the mirror); the docs Schema block publishes that dialect and no longer lists the phantom actions row. No renderer change; the three declared-but-unread keys cancelLabel / confirmLabel / confirmVariant are untouched and their readings are on the PR. Premise re-measured on merge-base 6eebc54b6: the four scope files are byte-identical to a3eb5d07a, the renderer reads exactly those four keys, and the asymmetry holds (read-dialect producers on an alert-dialog node: 3, incl. the component's own inputs and defaultProps; declared-dialect producers: 0; birth history via REST shows the renderer read cancelText/actionText from its first commit d3a881b77 on 2026-01-13 and the type declared the trio from its first commit c592efe36 on 2026-01-14, so the trio never had a reader). One premise detail moved and is reported, not built on: onConfirm / onCancel are already never-tombstones since objectui#6124, so the card's five inert declared keys are three authorable inert keys plus two retired tombstones today. Removal probe on the real carrier (the reading the follow-up turns on): deleting the trio is SILENT for all three shapes (fresh literal, lone-key widened value, overlapping-key widened value); the control carrier C1 (all-optional, no index signature) reproduces the objectui#7678 correction exactly (TS2353 / TS2559 / silent); C2 isolates the mechanism as BaseSchema's string index signature to any, which removes both the excess-property and the weak-type check; the required type only adds a constant TS2741 on the lone-key shape that fires identically on the live tree; the one channel that goes loud-to-silent is a wrong-typed value (cancelLabel: 123 and confirmVariant: ghost are TS2322 live, silent deleted). Per-key: cancelLabel and confirmLabel have a named live replacement (cancelText / actionText, prong 1 yes) and were taught only by a JSDoc @default (no docs row, no CHANGELOG line in alert-dialog context, no rename-ledger row; control triggerIcon 1 CHANGELOG hit); confirmVariant has NO live replacement (the read dialect cannot spell a button variant) and the same JSDoc-default-only prong 2. Process notes: the dispatch said the earlier attempt wrote nothing, but this session's earlier attempt had committed d161d78bb locally (unpushed) and filed objectui#7693; I audited that commit line by line against the ruling and current main, merged origin/main (no conflicts), and built on it rather than redoing it. The card arrived with assignees empty on the payload read (the PM-side half state), left alone as instructed. Scope beyond the four fenced files is limited to the DoD's own artifacts: one new pin test, one changeset, and ledger edits to three existing pin tests that count or enumerate mirror handler keys and docs rows (handler-keys-json-refusal-6124, zod-mirror-parity KnownDrift, overlay-node-slot-doc-types-7082); named on the PR. PR labels read back: needs:contract-review plus auto labels package: types and tests. Draft PR, not flipped ready, not enqueued. All GitHub writes went through the REST channel (probe 200 at run start).", "tests": "Head 3c10879a1 (final commit), through scripts/pm/os-verify-lock.sh: build closure `pnpm --filter '@object-ui/components^...' build` VERDICT command-exit 0 (types rebuilt: dist completeness 120 emitted files verified); `pnpm --filter @object-ui/types type-check` (tsc --noEmit, tsconfig.examples.json, tsconfig.test.json) exit 0; `pnpm exec vitest run packages/types/ --maxWorkers=2` from the repo root: Test Files 106 passed (106), Tests 1783 passed (1783); `pnpm --filter @object-ui/types lint`: 0 errors, 269 pre-existing warnings. Cross-package reverse verification through the rebuilt dist d.ts: probe file in packages/components/src with cancelText: 123 made `pnpm --filter @object-ui/components type-check` exit 2 with exactly one error, src/__probe-7104.ts(3,70): error TS2322: Type 'number' is not assignable to type 'string'; probe deleted, type-check exit 0 (one probe prediction was wrong: content: 123 predicted red, observed silent, because SchemaNode admits number). Unlocked gates on the same head: check:doc-types (188 docs, Every documented component type is registered), check:doc-fences, check:control-bytes (6266 tracked text files), check-doc-links (17 scan roots), check-changeset-presence (6 source files of 1 released package, 1 changeset), check-changeset-no-major, check-changeset-overwrite, check-changeset-fixed: all exit 0. NOT MEASURED: check:doc-snippets (needs all packages built, repo-wide); declared narrowing: population = gate DOCS_ROOT content/docs (184 md/mdx) + 39 package READMEs + root README; the gate compiles ts/tsx/typescript fences only and the single doc in this diff has 1 plaintext fence and 0 such fences; additive optional members cannot move a type-only import; CI is the authority. Ablation B (red first): overlay.ts, overlay.zod.ts, alert-dialog.mdx put back to 6eebc54b6 while tests stayed at HEAD, restore under trap with absolute paths; mutation proven by anchored counts (cancelText?: string; 0, mirror cancelText: 0, page actions?: BaseSchema 1, control cancelLabel?: string; 1) and blob movement (9df1af15e to 2ea4efd35, 5e4caa80f to 8d8d7b8b3, e6af5fe13 to bf37523c8); predicted 13 red / 26 green, observed vitest `Tests 13 failed | 26 passed (39)`; tsc -p tsconfig.test.json: 10 errors in the pin file plus zod-mirror-parity.test.ts(1432,14) TS2322 not assignable to type never (the ratchet fires in the tsc channel only, as PR #7684 disclosed); restore proven: all three blobs equal HEAD blobs, git diff HEAD empty. Ablation A (removal probe): the trio deleted from overlay.ts on disk, anchored counts 0/0/0 with controls actionText 1 and cancelText 1, blob 9df1af15e to 988c34a92; tsc over a 4-carrier x 3-shape probe on LIVE and DEL trees: C1 TS2353/TS2559/silent, C2 silent/silent/silent, C3 TS2353/TS2741(constant)/silent, C4 (real) silent/TS2741(constant)/silent; controls cancelLabel: 123 and confirmVariant: ghost TS2322 live to silent deleted, actionText: 123 TS2322 both, onConfirm/onCancel function values TS2322 (never) both; tsconfig.test.json under DEL red at exactly the three still-declared type pins (lines 127, 128, 130); restore proven by blob equality and empty git diff HEAD. Neither ablation involves a dist path (both legs compile from src), so no rebuild was owed for them; the union build above is the one dist rebuild and the components probe read it. Lock verdict lines: command-exit 0 for the build; the sequenced runs print per-step exits (all 0) because they were joined with ';'. All wall-clock readings are shared-box figures.", "mcp_calls": "0 — every GitHub read went through the public-page payload, git, or REST (core bucket), and every write (PR create, label add, report comment) through REST", "open_questions": [ { "question": "Disposition of the three inert declared keys (the follow-up card, not this PR): on AlertDialogSchema their removal is silent in every value shape because BaseSchema carries a string index signature to any, and the only channel removal makes quieter is a wrong-typed value (TS2322 live, silent removed). cancelLabel / confirmLabel meet prong 1 (cancelText / actionText); confirmVariant meets neither prong beyond a JSDoc @default and has no live replacement.", "options": [ "A: never-tombstones for all three (this package's convention; keeps the wrong-typed-value channel loud and the pointer to cancelText / actionText legible; confirmVariant's tombstone would point at nothing live and say so)", "B: never-tombstones for cancelLabel / confirmLabel, outright removal for confirmVariant (prong-driven split within one carrier)", "C: make confirmVariant live in the renderer first (a feature, manual floor per objectui#7693's triage), then decide the other two" ], "recommendation": "A, because the removal-probe reading shows deletion is silent on this carrier in all three shapes while the existing onConfirm / onCancel tombstones are measurably loud today (TS2322), and a route split inside one carrier is the defect objectui#7678 names; C is a separate feature card either way." } ], "out_of_scope_findings": [ "filed as #7693 (by this session's earlier attempt, during this run): the four schema-catalog fixtures the docs page embeds author an actions array no surface carries, so the page's own examples render an empty footer; the conversion is lossy (destructive/ghost button variants have no spelling in the read dialect); PM-triaged domain:devx / priority:p2" ] }
Generated by Claude Code
⛔ Correction #3 — the index signature defeats BOTH loud channels, so on this repo's component schemas removal is fully silent
Third revision of this discriminator by the seat that wrote it. Measured on card #7104 (PR #7700) by an
os-devseat atclaude-fable-5-1, and re-verified directly by me onorigin/main6e8863093before posting.What each revision said
version claim about deleting an optional member from a surviving carrier original "can arrive through a widened value and nothing complains" — silent correction #1 (comment 5548864634) silent only when the value shares a key with the carrier; a lone-key widened value trips TS2559, a fresh literal TS2353 this correction correction #1 holds only on a carrier with no index signature. BaseSchemacarries[key: string]: any, so on every schema extending it deletion is silent in all three shapesThe measurement
A 4-carrier × 3-shape probe (fresh literal / lone-key widened value / overlapping-key widened value), run on a LIVE tree and a tree with the members deleted:
carrier fresh literal lone-key widened overlapping-key widened C1 all-optional, no index signature TS2353 TS2559 silent C2 all-optional with [key: string]: anysilent silent silent C4 the real AlertDialogSchemasilent TS2741 (constant — fires identically on the live tree) silent C1 reproduces correction #1 exactly, which is what makes C2 a reading rather than a broken instrument: the two differ only by the index signature, and it is the index signature that removes both excess-property checking and the weak-type check.
Verified by me, independently of that probe, on
origin/main6e8863093:packages/types/src/base.ts:440declares[key: string]: any, andpackages/types/src/overlay.ts:83readsexport interface AlertDialogSchema extends BaseSchema.Why this matters far beyond one card
Essentially every component schema in this repo extends
BaseSchema. So for the carrier branch of this discriminator — the branch that covers every retirement of an optional member from a component schema — the motivating premise is not "usually silent" but silent, in every value shape. That is the strongest possible case for a tombstone, and it is stronger than either earlier version of this rule said.The one channel deletion makes quieter, measured on the same probe: a wrong-typed value.
cancelLabel: 123andconfirmVariant: 'ghost'read TS2322 while declared and go silent once deleted. So the tombstone's value on these carriers is keeping the type error loud, not the presence error — the presence error does not exist here at all.Corrected wording for the carrier branch
A
?: nevertombstone is available only on a surviving carrier. On a carrier extendingBaseSchema— which is every component schema in this package — deleting an optional member is silent in every value shape, because the[key: string]: anyindex signature removes both excess-property checking and the weak-type check; the only channel the tombstone keeps loud is a wrong-typed value. Tombstone when either prong holds. A whole exported type name has no carrier; there the type-level tombstone (export type X = never) trades channels with removal rather than dominating it, and this package has no alias-tombstone convention — remove outright.⛔ Do not inscribe correction #1's TS2559 sentence into
complex.ts:967ormobile.ts:129. It is true of a bare interface and false of this repo's schemas, which is the only population those comments are read in.Standing lesson, recorded because it is now three-for-three
Every version of this rule that I wrote from reasoning rather than from a probe has been wrong, and each was corrected by someone who ran the compiler. The prongs were argued; the carrier precondition was argued; the silence claim was argued twice. The parts that survived are the parts that were measured. A discriminator about compiler behaviour belongs to whoever runs the compiler.
Generated by Claude Code
- added a commit that references this issue
on Sep 5, 2026 - added a commit that references this issue
on Sep 9, 2026
Found while implementing #7082, which corrects the type name on
alert-dialog.mdx:30. Filed rather than folded in: #7082 is fenced to docs rows, and this is a declaration/renderer divergence.Measured on
origin/main2c3cd1b7572a8aca2958da3551c990164186c9c1.The three surfaces
AlertDialogSchema(packages/types/src/overlay.ts:78-127, mirrored atpackages/types/src/zod/overlay.zod.ts:42-55) declares, pasttype/title/description/trigger/defaultOpen/open:cancelLabel?: stringconfirmLabel?: stringconfirmVariant?: 'default' | 'destructive'onConfirm?: () => void | Promise<void>onCancel?: () => voidpackages/components/src/renderers/overlay/alert-dialog.tsxreads, past the ones both agree on:schema.content(rendered as the dialog body)schema.cancelText(drivesAlertDialogCancel)schema.actionText(drivesAlertDialogAction)schema.onAction(the action'sonClick)And
content/docs/components/overlay/alert-dialog.mdx:33publishes a third spelling again:actions?: ComponentSchema[]schema.actionsappears nowhere in the rendererSo the confirm/cancel affordance has three names on three surfaces and no two of them agree.
Why it matters, in the direction that bites an author
The published TS type is what an author's editor reads. Today:
cancelLabel/confirmLabel/onConfirm— the keys the type declares and the ones an editor completes — produces no cancel button, no confirm button and no handler. The renderer only drawsAlertDialogCancelwhenschema.cancelTextis truthy, andAlertDialogActionwhenschema.actionTextis. So a document written strictly against the shipped type renders a dialog with an empty footer.cancelText,actionText,onAction,content) is only accepted becauseBaseSchemacarries[key: string]: any— no editor suggests them, and no page names them.defaultProps(same file) shipscancelText/actionText, so the component's registered default is written in the undeclared dialect.This is the same shape as #6773 and #6788 (documents authoring a key the renderer never reads), with the extra wrinkle that here the declaration is on the losing side too.
Why nothing red covers it
zod-mirror-parity.test.tscompares the TS declaration to its Zod mirror. These two agree — both carrycancelLabel/confirmLabel/onConfirm. The renderer is not a party to that test.check:doc-snippetscompilests/tsx/typescriptfences; theactionsrow is in aplaintextfence, so no gate parses it (finding: JSON doc snippets are checked by nothing — the ts/tsx gate cannot see them, andBaseSchema.passthrough()makesobjectui validateaccept arbitrary undeclared keys #5250 / finding(docs): TypeScript examples fenced as plaintext escape check-doc-snippet-types — 211 blocks in 122 files, and #5044 proved one of them taught a type error under a gate that reports the file as covered #5867).check:doc-typesreads only thetypestring literals out of docs code blocks.content/docs/componentspages #6150's sweep declared 13 renderer-read keys that no type carried, butalert-dialogis not among the components its ledger (packages/types/src/__tests__/undeclared-but-consumed-keys-6150.test.ts) pins.Options
cancelLabel,confirmLabel,confirmVariant,onConfirm,onCancel) and retire the undeclared dialect, updatingdefaultPropsand any fixture. Contract-first: the published type wins, and the docs row foractionsis deleted as a phantom.content,cancelText,actionText,onAction) and retire the inert declared keys under ADR-0049 enforce-or-remove.content(a real body slot the sibling overlay types all declare) and resolve the button pair one way.(a) looks right on contract-first grounds — the declared names are also the better ones, and
confirmVariantis a capability theactionTextdialect cannot express. But it changes shipped runtime behaviour for any document written in the working dialect, so it wants a triage decision rather than a drive-by. Either wayalert-dialog.mdxis owed a rewrite of its whole Schema block, not just the two rows #7082 touched.This is a concrete instance of the general class #4631 names.
Refs: #7082 · #4631 · #6150 · #6773 · #6788.