fix(types): declare the four alert-dialog keys the renderer reads (objectui#7104) - #7700
Conversation
…jectui#7104) `AlertDialogSchema` now declares `content`, `cancelText` and `actionText` on both faces, and `onAction` as a runtime slot (callable on the TS face, refused by name in the zod mirror — the objectui#6124 shape). None of the four was declared anywhere; the renderer, its registered `inputs` and its `defaultProps` were already written in this dialect. The docs Schema block now publishes it and no longer lists the phantom `actions` row no surface carried. The three declared-but-unread keys (`cancelLabel` / `confirmLabel` / `confirmVariant`) are untouched: retiring them is a narrowing with its own card. The new pin test records their state so that card re-derives it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
Sync the branch to the current main (6 commits; no overlap with the objectui#7104 scope files) before re-measuring and re-verifying. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
…header The pin file's provenance line now names both the base it was written on (a3eb5d0) and the merge-base the branch was re-measured and pushed from (6eebc54): the four scope files are byte-identical between the two. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract review —
|
| change | verdict | deciding measurement |
|---|---|---|
content?: SchemaNode | SchemaNode[] (TS) |
RIGHT | Same spelling as DialogSchema/SheetSchema/DrawerSchema (overlay.ts:46/205/255). Read is renderChildren(children: any) (lib/utils.tsx:18): falsy→null, string/number→text, array→nodes, object→SchemaRenderer. Declared domain is not wider than the read (the read takes any); it admits boolean only because SchemaNode does, package-wide. content: 123 is silent on the TS face (reproduced) — the author's prediction was wrong, the declaration is right: renderChildren(123) renders 123 as text, a legitimate body. content: { label: 'x' } TS2322 (no type); content: () => 1 TS2322. |
content (mirror) |
RIGHT | SchemaNodeSchema is BaseSchemaCore | string | number | boolean | null | undefined; measured: number/string/boolean/null/array GREEN and survive, object-without-type REFUSED invalid_union at content — was GREEN at the merge-base. |
cancelText?: string (TS) |
RIGHT | Matches schema.cancelText && truthiness gate, no renderer default; cancelText: 123 TS2322 (absorbed by the index signature before). |
cancelText (mirror) |
RIGHT | z.string().optional(); measured GREEN→REFUSED for 123 and null, '' stays GREEN (and draws no button, consistent with the docblock). |
actionText?: string (TS) |
RIGHT | Same shape; actionText: 123 TS2322 on LIVE and DEL trees. |
actionText (mirror) |
RIGHT | ['Continue'] GREEN at base → REFUSED invalid_type now. |
onAction?: () => void (TS runtime slot) |
RIGHT | Callable kept (KeepsFunction pin); assignable to a button onClick (measured silent); thunk and async thunk accepted; string TS2322, object TS2353. Arity follows the package's own button click slot ButtonSchema.onClick?: () => void | Promise<void> (form.ts:33, a 6124 runtime slot forwarded to the DOM <button>); @object-ui/types has no React dependency to name the event. See ③ for the one shape this refuses. |
onAction (mirror) |
RIGHT | handlerKeyRefusal('onAction','runtime-slot'): string/object/function all REFUSED custom at onAction, message names action:button; all three were GREEN at the merge-base (a live function parsed to undefined). |
docs Schema block publishes content/cancelText/actionText |
RIGHT | Rows match the declaration's own spelling and optionality; pinned. |
docs actions?: BaseSchema[] row removed |
RIGHT | Declared on neither face (shape keys measured), schema.actions absent from the renderer; a phantom in every direction. |
docs title/description → optional |
RIGHT | Declared title?/description? (overlay.ts:88/92). trigger stays published required against a trigger? declaration — the deferral 7082 deliberately recorded, not this PR's. |
docs publishes open as live |
RIGHT | SchemaRenderer.tsx:1576 spreads componentProps; the renderer spreads {...props} onto the Radix root (alert-dialog.tsx:26). |
docs prose for onAction (no row) |
RIGHT | Same convention as dialog.mdx, which publishes no onOpenChange row. |
cancelLabel/confirmLabel/confirmVariant untouched |
RIGHT | Still declared both faces, still read by nothing (pinned); their removal is a narrowing with its own card. |
onConfirm/onCancel untouched |
RIGHT | Already ?: never at 6eebc54b6:overlay.ts:128/137 and handlerKeyRefusal('retired'); refused at base and head alike. |
| No renderer change | RIGHT | git diff 6eebc54b6 3c10879a1 -- alert-dialog.tsx empty. |
handler-keys-json-refusal-6124 ledger +1 (60 sites) |
RIGHT, convention-entailed | With the file at base and source at HEAD the file stays green — its site ledger is self-referential (the source census scans only on*: z.function(), which is exactly why the site must be ledgered in the PR that adds it. Real handlerKeyRefusal( sites in the mirrors: 67→68; the 59→60 ledger tracks the delta (the 8-site gap is pre-existing). |
zod-mirror-parity KnownDrift 'onOpenChange' | 'onAction' |
RIGHT, test-entailed both ways | Source at base + ledger at HEAD: TS2322 at :1432; source at HEAD + ledger at base: TS2322 at :1420 — both in the tsc -p tsconfig.test.json channel only, as #7684 disclosed. |
overlay-node-slot-doc-types-7082 drops the actions row |
RIGHT, test-entailed | File at base + page at HEAD: red on "AlertDialogSchema.actions is documented but declared nowhere". |
New pin alert-dialog-read-dialect-7104.test.ts |
RIGHT, not dark | See ablation below. |
Changeset @object-ui/types: minor |
RIGHT | See ②. |
Scope fence. Eight files: the four fenced, plus the changeset (CI Changeset Declaration requires one for a source change in a released package), the new pin (the DoD), and three ledger edits — two test-entailed, one convention-entailed. Nothing rides along.
Ablation (author's Ablation B, reproduced). overlay.ts, overlay.zod.ts, alert-dialog.mdx restored to 6eebc54b6 (anchors: cancelText?: string; 0, mirror cancelText: 0, page actions?: BaseSchema 1, control cancelLabel?: string; 1), tests at HEAD: vitest 13 failed / 26 passed (39) — the three membership legs, three wrong-typed legs, three onAction legs, four docs rows. tsc -p tsconfig.test.json: 7×TS2344 (lines 114-117, 123-125), TS2578 (139), TS7053 (180), TS2339 (221) in the pin file plus zod-mirror-parity.test.ts(1432,14) TS2322 … not assignable to type 'never'. Restore verified by empty status. HEAD state: 4 pin files / 332 tests green, tsc -p tsconfig.test.json 0, tsc --noEmit 0.
⭐ Removal probe, reproduced with my own lit controls (4 carriers × 3 shapes, LIVE tree then the trio deleted from overlay.ts lines 135-149 on disk — anchors 0/0/0, controls cancelText?:/actionText?:/onConfirm?: never 1 each — then restored to the HEAD blob 9df1af15e):
| carrier | fresh literal | lone-key widened | overlapping widened |
|---|---|---|---|
| C1 all-optional, no index signature | TS2353 | TS2559 | silent |
C2 all-optional + [key: string]: any |
silent | silent | silent |
C3 required type, no index signature |
TS2353 | TS2741 (missing type) |
silent |
C4 AlertDialogSchema, trio deleted |
silent | TS2741 (constant — fires on LIVE too) | silent |
C1 reproduces #7678 correction #1 exactly; C1→C2 differ only by the index signature, which removes both excess-property and weak-type checking. Wrong-typed channel: cancelLabel: 123, confirmLabel: 123, confirmVariant: 'ghost' TS2322 on LIVE, silent on DEL; cancelText: 123/actionText: 123 TS2322 on both; onConfirm/onCancel functions TS2322 on both. Correction #3 on #7678 stands as measured. One addition (③ item 6).
② Semver grading
Declared minor; correct level minor. AGENTS.md §版本号策略: majors track @objectstack, major is CI-refused (scripts/check-changeset-no-major.mjs, Changeset Bump Policy green), objectui's own breaking changes ship as minor with the semantics in the body. The changeset's body spells the mirror narrowing correctly.
This is a widening in intent (four keys go from index-signature/passthrough to declared) and narrows in exactly these places, measured before/after: mirror — 7 documents flip GREEN→REFUSED at their own path (content object-without-type; cancelText 123 and null; actionText array; onAction string, object, function); TS face — wrong-typed values at the three keys, and onAction given a handler with a required event parameter ((e: {preventDefault(): void}) => void TS2322; it compiled through the index signature before). The last is covered by the changeset's general sentence but not named; see ③. Nothing narrows at runtime; undeclared keys still pass through on both faces.
③ Boundary-flag disposition
- Card options (a)/(b)/(c); PM's "(b) ⇒ handoff to
domain:spec". The ruling took the declare half of (b) and deferred the retire half. Direction confirmed above. The handoff is a process question for the PM seat; the PR is labelledpackage: types, draft,needs:contract-review, not enqueued. - PM's ⛔ "any disposition must also move
inputs". Satisfied without a move:inputsanddefaultPropsalready ship the read dialect, now pinned, so thesdui.manifest.json/sdui-intrinsics.d.tschain teaches the declared spelling. - Card: the page owed a whole Schema-block rewrite. Delivered.
confirmVariantis a capability the read dialect cannot spell. Still true; the PR leaves the capability absent rather than declaring a second dialect; finding(examples,docs): the four alert-dialog schema-catalog fixtures author anactionsarray no surface carries — the docs page's own examples render an empty footer #7693's triage calls making it live a feature.- finding(examples,docs): the four alert-dialog schema-catalog fixtures author an
actionsarray no surface carries — the docs page's own examples render an empty footer #7693. The four fixtures the page embeds still authoractionsand render an empty footer, so the page's Schema block now contradicts its own embedded examples until finding(examples,docs): the four alert-dialog schema-catalog fixtures author anactionsarray no surface carries — the docs page's own examples render an empty footer #7693 lands. Correctly out of scope (outside the fence, lossy conversion), and pinned as that card's filed premise so the fix goes red here. - Author's open question — disposition of the trio (A/B/C). Leaving them in this PR is right: retiring is a narrowing on both published faces with its own grade, and the probe shows no presence hazard is created by waiting (deletion changes nothing on presence). One reading for the follow-up, from the same probe: on this carrier a
?: nevertombstone makes presence with any value loud —onConfirm: () => {}is TS2322 today — which is a channel outright deletion cannot produce here at all. That is the decisive difference between the tombstone routes and removal on aBaseSchemacarrier, and it is stronger than correction Add public roadmap, VitePress documentation site, and GitHub Pages deployment #3's "keeps the wrong-typed channel loud". I do not pick among A/B/C. - Premise moved. Verified:
onConfirm/onCancelare?: never+handlerKeyRefusal('retired')at the merge-base. Three authorable inert keys + two retired tombstones. onActionarity (disclosure). The TS slot refuses event-typed handlers, including Radix's documentedevent.preventDefault()-to-keep-open idiom onAlertDialog.Action; the package'sEventHandlers.onClick?: (event?: any) => void | Promise<void>(base.ts:828) accepts all four probed shapes and is equally forwardable. This matchesButtonSchema.onClick's existing arity-0 convention, so it is a package-wide question, not this PR's defect; one clause in the changeset naming it would close the disclosure gap.check:doc-snippetsnot measured locally. CI's Doc Snippet Type Check is green on the head.mergeable_state: behind.mainmoved but not under the eight paths; the parity ratchet is not stale at this moment. The queue rebuilds onmain; ifmainretires a ledgered key first the ratchet will say so in thetscchannel.
VERDICT: PASS
Generated by Claude Code
Fixes #7104
What
AlertDialogSchemanow declares the four keys thealert-dialogrenderer reads.content(the dialog body, a SchemaNode or a SchemaNode array like every sibling overlay),cancelTextandactionText(the footer's two labels; each button renders only when its label is set, no renderer default) are declared on BOTH faces (packages/types/src/overlay.ts,packages/types/src/zod/overlay.zod.ts).onAction(the confirm button's click handler) is declared as a RUNTIME SLOT in the objectui#6124 shape: callable on the TypeScript face, refused by name in the zod mirror throughhandlerKeyRefusal()because JSON has no function value. No renderer change. The docs pagecontent/docs/components/overlay/alert-dialog.mdxpublishes the read dialect in its Schema block and no longer listsactions, a key no surface ever carried.⛔ Neither dialect is declared twice. The three declared-but-unread keys
cancelLabel/confirmLabel/confirmVariantare untouched here: retiring them is a narrowing with its own card; their readings are below so that card can be decisive.Premise re-measured on the current merge-base
6eebc54b6The card measured
2c3cd1b7. The four scope files are byte-identical betweena3eb5d07a(where this branch was cut) and6eebc54b6(its merge-base at push time;git diff --statover the four paths is empty). Renderer reads at6eebc54b6:schema.content(line 35),schema.cancelText(37),schema.actionText/schema.onAction(38); registeredinputsnamecancelText/actionText/content(49, 50, 59) anddefaultPropsshipscancelText: 'Cancel'/actionText: 'Continue'(67, 68). One part of the card's premise has moved:onConfirm/onCancelare no longer() to voiddeclarations; since objectui#6124 they are?: nevertombstones on the TS face andhandlerKeyRefusal('retired')in the mirror. So "five inert declared keys" is three authorable inert keys plus two already-retired tombstones today. The ruling's direction is unaffected.The asymmetry, verified rather than taken
In-repo producers on an alert-dialog node (
git grep, tracked files,node_modulesanddistexcluded): read dialectcancelText/actionText— 3 (alert-dialog.tsx:49-50inputs,:67-68defaultProps,packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx:719); declared dialectcancelLabel/confirmLabel/confirmVariant— 0 (every other hit for those spellings is a different carrier:FormSchemainform.ts/schema-reference.md:271,297/plugin-form.mdx:61/plugin-form/README.md:175,BulkAction.confirmLabelatobjectql.ts:497, and the retired structured-confirm object named in thecrud.ts:111tombstone). Birth history, read through REST at the first commit of each file: the renderer readcancelText/actionText/content/onActionin its first commitd3a881b77(2026-01-13) and the type declaredcancelLabel/confirmLabel/confirmVariant/onConfirm/onCancelin its first commitc592efe36(2026-01-14) — the two surfaces were born disjoint one day apart, and the declared trio has never had a reader. Converging the other way would blank the footer of every document written the way the component itself teaches.Red first (Ablation B, on the pushed head)
The pin file
packages/types/src/__tests__/alert-dialog-read-dialect-7104.test.tswas written before the schema edit and re-run with the three implementation files (overlay.ts,overlay.zod.ts,alert-dialog.mdx) put back to6eebc54b6while the tests stayed at HEAD — restored under a trap with absolute paths; mutation proven on disk (anchored counts:cancelText?: string;0 inoverlay.ts,cancelText:0 in the mirror,actions?: BaseSchema1 in the page; controlcancelLabel?: string;1; blobs9df1af15eto2ea4efd35,5e4caa80fto8d8d7b8b3,e6af5fe13tobf37523c8), restore proven (all three blobs equal the HEAD blobs,git diff HEADempty). Predicted 13 red / 26 green under vitest — observedTests 13 failed | 26 passed (39). Undertsc -p tsconfig.test.json: 10 errors in the pin file (seven TS2344 on the type-level pins, TS2578 unused ts-expect-error, TS7053 and TS2339 on the missing mirror members) pluszod-mirror-parity.test.ts(1432,14): TS2322 ... "overlay.zod.ts#AlertDialogSchema" is not assignable to type 'never'— the TS/Zod ratchet fires, and it fires in thetscchannel, as PR #7684 disclosed. After the change: 39 passed, tsc exit 0.Ablation A — the removal probe the follow-up turns on
The three inert keys were deleted from
overlay.tson disk (anchored counts 0 / 0 / 0, controlsactionText?: string;1 andcancelText?: string;1; blob9df1af15eto988c34a92; restored under trap, blob back to the HEAD blob,git diff HEADempty) and a probe was compiled withtscon the LIVE and DEL trees. Four carriers, three value shapes; "widened" means aconstinferred first and assigned second.[key: string]: anytype, no index signaturetype)AlertDialogSchemaitself, trio deleted on diskC1 reproduces the correction on objectui#7678 (comment 5548864634) exactly. On this carrier, deleting the three keys is silent in all three shapes. C2 isolates the mechanism:
BaseSchema's[key: string]: anyalone removes both the excess-property check and the weak-type check; the requiredtype(C3) only adds a constant TS2741 for the lone-key shape that fires identically on the live tree. The one channel that moves is the wrong-typed value:cancelLabel: 123andconfirmVariant: 'ghost'are TS2322 on LIVE and silent on DEL, whileactionText: 123stays TS2322 on both (declared by this PR, untouched by the mutation) and a function under the two existing tombstonesonConfirm/onCancelis TS2322 (never) on both. Under DEL,tsc -p tsconfig.test.jsongoes red at exactly the three "still declared" type pins in the new file (lines 127, 128, 130) — the retirement card re-derives those on purpose.Per-key readings for the follow-up disposition (lit controls stated above)
cancelLabeltriggerIcon1 in root CHANGELOG; the onlycancelLabellines areFormSchema.submitLabel/cancelLabel)@default 'Cancel'cancelText(declared here)confirmLabel@default 'Confirm'actionText(declared here)confirmVariant@default 'default'onConfirm?: nevertombstone (objectui#6124) pointing atonActiononCancel?: nevertombstone (objectui#6124)Designer: the registration's
inputscarrycancelText/actionText/contentand none of the trio (pinned).openis published on the page because it IS live:SchemaRendererspreads the non-metadata top-level keys as props (componentProps) and the renderer spreadspropsonto the Radix root, the same route theonOpenChangeruntime slot uses; the inert trio travels that route too and Radix drops it.Clause ② — yes; graded
minorDeclared keys are validated even under
.passthrough(), so three documents that parsed green before are refused at their own path now:cancelText/actionTextwith a non-string,contentwith a value that is not a node or node array, andonActioncarried at all. A well-typed read-dialect document parses exactly as before. Spelled out in.changeset/7104-alert-dialog-read-dialect-declared.md;majoris CI-refused (AGENTS.md version alignment). Draft PR,needs:contract-reviewon it, not flipped ready, not enqueued.Gates on the pushed head
3c10879a1(after the final commit)Through
os-verify-lock.sh(verdict linesVERDICT command-exit 0for the build, per-step exits 0 for the sequenced runs):pnpm --filter '@object-ui/components^...' build— types rebuilt,✓ dist completeness: 1 package(s) complete (120 emitted files verified)pnpm --filter @object-ui/types type-check(three tsc projects, the tests included — the base ablation above proves tsc reads the pin file) — exit 0pnpm exec vitest run packages/types/ --maxWorkers=2from the repo root —Test Files 106 passed (106),Tests 1783 passed (1783)pnpm --filter @object-ui/types lint—0 errors, 269 warnings(all pre-existingno-explicit-any)dist/*.d.ts: a probe file inpackages/components/srcwithcancelText: 123madepnpm --filter @object-ui/components type-checkexit 2 with exactlyTS2322: Type 'number' is not assignable to type 'string'at that line; after deleting the probe, exit 0. One probe prediction was wrong:content: 123was predicted red and is silent, becauseSchemaNodelegitimately admitsnumber.Outside the lock:
check:doc-types✅ (188 docs, 45 table keys registered),check:doc-fences✅,check:control-bytes✅ (6266 tracked text files),check-doc-links✅ (17 scan roots),check-changeset-presence✅,check-changeset-no-major✅,check-changeset-overwrite✅,check-changeset-fixed✅.NOT MEASURED locally:
check:doc-snippets. It needs every package built and is a repo-wide scan. Declared narrowing: population is the gate's ownDOCS_ROOT(content/docs, 184 md/mdx) plus the 39 package READMEs and the root README (ROOT_PAGES); the gate compiles ts/tsx/typescript fences only, and the single doc file in this diff carries 1plaintextfence and 0 ts/tsx/typescript fences; the type change is additive optional members and cannot move a type-only import elsewhere. CI's doc-snippet gate is the authority.Consumer sweep, direction stated: downstream consumers that name
AlertDialogSchemaoutsidepackages/types= 1 file (packages/components/src/renderers/overlay/alert-dialog.tsx, measured by grep), and@object-ui/componentstype-checks green against the rebuilt dist. Repo-widepnpm lintnot run (CI owns it); the only linted package in this diff is@object-ui/types, run above.Scope
Four fenced files, plus the test and changeset the definition of done owes: one new pin file, and ledger edits to three existing pin tests that count or enumerate the mirror's handler keys and the docs rows (
handler-keys-json-refusal-6124.test.tsgains theonActionruntime-slot site,zod-mirror-parity.test.tsledgersonActionas a KnownDrift key on the existing pair,overlay-node-slot-doc-types-7082.test.tsdrops theactionsrow it recorded as undeclared). Nothing else is touched.Out of scope — filed, not fixed here
actionsand render an empty footer; the conversion is lossy (two fixtures usevariant: destructive, which the read dialect cannot spell). Filed by this seat during this run; PM-triageddomain:devx/priority:p2.Dev seat: Claude Code session
session_01KbJQ1y1J12nZxYzFWhP8Q3, dispatched by thedomain:uiPM seat.🤖 Generated with Claude Code
https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
Generated by Claude Code