Uh oh!
There was an error while loading. Please reload this page.
feat(types): declare the 13 renderer-read keys that no shipped type declared - #6945
Merged
Merged
Conversation
The undeclared-but-consumed census (objectui#6150) found 68 documented keys that are not declared members of their shipped type; 13 were shown to be genuinely READ by the renderer. Those 13 are declared here on both faces — the TypeScript interface and, for 12 of them, the hand-written zod mirror. Key membership is not widened: every touched mirror extends the `.passthrough()` `BaseSchema`, so all 13 already parsed green and already survived the parse, admitted unexamined. What changes is declaration (the key becomes a member of the shipped type) and, for the 12 mirrored keys, value enforcement — which in the value dimension is a narrowing. `TreeViewSchema.onNodeClick` is INVOKED, not read as a value, so it gets no mirror: objectui#6152 routes that class to `RuntimeOnlyDeclared` in `zod-mirror-parity.test.ts`, and this is the first pair to sit there without also sitting in `UnmirroredDeclared`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
`SchemaNode` itself admits `null | undefined`, so wrapping the property in `NonNullable` stripped those limbs out of the union and compared a different type — `tsc -p tsconfig.test.json` read `Type 'false' does not satisfy the constraint 'true'`. Read raw the two sides are equal, and the guard still bites: an undeclared `trigger` resolves to `any` through `BaseSchema`'s index signature, and `Equal< any, … >` is false. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
…r `any` `pnpm exec eslint .` flagged two `no-explicit-any` warnings introduced by the `Case.mirror` handle. The structural type it needs is small and writable, so write it: `data` is the parsed document, `error` is the issue list the refusal assertions read. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
Contributor
✅ 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
|
This was referenced Aug 31, 2026
os-sam
marked this pull request as ready for review
August 31, 2026 03:11
github-merge-queueBot
removed this pull request from the merge queue due to failed status checks
Aug 31, 2026
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 31, 2026
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#6150
needs:contract-review). Staying in draft; not marked ready, not enqueued, no auto-merge.Head this PR's evidence was measured on:
065b16b98. Base:40c479af2.⛔ Read this before the table: the accept set did NOT widen in the key dimension
The dispatch order asked me to establish this per schema rather than assume one answer, and to say so plainly if
passthroughmeant the before-state already accepted. It did. Measured, not assumed:All 8 touched mirrors extend the zod
BaseSchema, which ends.passthrough(), and.extend()carries that policy through. Read off the built mirrors,catchallisz.unknown()on all 8. So before this PR every one of the 13 keys already parsed green and already SURVIVED the parse — admitted unexamined, neither refused nor stripped.So the PR does not claim an accept-set change it did not make. What it actually changes is two things:
BaseSchemaends with[key: string]: any(finding(types): BaseSchema's[key: string]: anyleaves every component schema open, so a "declare the surface" fix can never reject a misspelled TOP-LEVEL key #5155), soschema.triggeron a type that never declaredtriggerresolves toany.{ type: 'text', content: 42 }parsed green before and is refused atcontentnow. That is a behaviour change for documents carrying a wrong-typed value under one of these 13 names, and it is the point —declared === enforced.Because acceptance cannot tell "declared" from "admitted unexamined" under passthrough, membership is asserted on the mirror's own
.shape, never on parse acceptance — the formobject-grid-title-mirrored.test.tsestablished for #6639.Per-key before / after — the table triage asked for
Probed by parsing real documents through the built
packages/types/dist/zod/index.zod.jsin two worktrees:40c479af2(before) and065b16b98(after). Control document per type = required keys only, carrying none of the 13..shapeTextSchema.contentcontentCarouselSchema.optsoptsCarouselSchema.orientationorientationCarouselSchema.itemClassNameitemClassNameFilterBuilderSchema.wrapperClasswrapperClassTreeViewSchema.nodesnodesTreeViewSchema.titletitleTreeViewSchema.onNodeClickCheckboxSchema.requiredrequiredFileUploadSchema.buttonTextbuttonTextFileUploadSchema.wrapperClasswrapperClassHoverCardSchema.alignalignContextMenuSchema.triggertrigger"Widened only what I meant to", per schema: the rightmost column is that proof. On all 8 mirrors, a document carrying an undeclared key of the same wrong type is still admitted and still survives the parse — before AND after. The unknown-key policy of every touched mirror is exactly what it was. This is also pinned per key in the test file, so it cannot silently stop being true.
One extra measurement, because declaring
nodesinvites a wrong inference: a{ type: 'tree-view', nodes: [...] }document with nodatais REFUSED atdata, before and after.datastays required on both faces. Declaringnodesrecords the read; it does not by itself make anodes-only document legal. Relaxingdatais an accept-set change and a separate ruling.Premise re-derived, not inherited
Module resolution path, stated before measuring.
packages/types/distdid not exist in the fresh worktree, so I built it (pnpm --filter @object-ui/types build, verdictcommand-exit 0) and measured throughpackages/types/dist/index.d.ts— the census's own path,derivePackageTypePaths()style — and independently throughpackages/types/src/index.ts. Declared-member sets for all 8 types were identical betweendistandsrc, so no stale-build reading is in play. Members were resolved with the TypeScript compiler API (getPropertiesOfType), which sees inherited members and reports thestringindex signature separately, so[key: string]: anycould not mask the answer.Result: 13/13 still undeclared on base. After the change, re-measured through the rebuilt
dist/index.d.ts: 13/13 declared, index signature still present on all 8 (so nothing aboutBaseSchemamoved).Every read re-verified at its cited site. All 13 reads are still present. One line number drifted; the read is the fact:
TextSchema.contentbasic/text.tsx:51,56:51,56— unchangedCarouselSchema.opts/orientation/itemClassNamecomplex/carousel.tsx:23 / 24 / 30FilterBuilderSchema.wrapperClasscomplex/filter-builder.tsx:37TreeViewSchema.nodes/onNodeClick/titledata-display/tree-view.tsx:105 / 98,99 / 115,117CheckboxSchema.requiredform/checkbox.tsx:45,49FileUploadSchema.buttonText/wrapperClassform/file-upload.tsx:123 / 78HoverCardSchema.alignoverlay/hover-card.tsx:24ContextMenuSchema.triggeroverlay/context-menu.tsx:64:95— drifted, read intactNothing was dropped. No key's reader had been removed.
READ vs INVOKED.
TreeViewSchema.onNodeClickis invoked —if (schema.onNodeClick)thenschema.onNodeClick(node), withnodethe clickedTreeNodeand the return value discarded. Its declared type is therefore the call signature(node: TreeNode) => void, not a value shape, and it is pinned as such with an invariant equality assertion. The other 12 are value reads.Three declarations that are not "declare what is read" on autopilot
Each says so in its own doc comment; all three are flagged for the contract reviewer.
CarouselSchema.optsis an OPEN record (string keys, unknown values), not the docs page's two-key shape. The renderer forwards the whole bag verbatim to embla (opts={schema.opts}), so every other embla option authored today reaches the library and works. Declaring the documented pair would refuse those documents — an accept-set narrowing on a published surface, which is a ruling, not a declaration. The card itself flagged this key as the one that "may want a narrower shape"; I declined to narrow it without a ruling.ContextMenuSchema.triggeris OPTIONAL although the docs page shows it required. The renderer substitutes a placeholder node when it is absent, so every trigger-less document is legal today and declaring it required would refuse them.TreeViewSchema.onNodeClickgets NO zod mirror. A function cannot appear in an authored JSON document, so it is a runtime slot; 121 declared-but-unmirrored keys across 16 schema pairs — the lane #6058's new UnmirroredDeclared ledger made visible #6152 ruled that class is never mirrored and is recorded inzod-mirror-parity.test.ts'sRuntimeOnlyDeclaredinstead — the step-3 exception that file's own header spells out. This is the first pair to sit inRuntimeOnlyDeclaredwithout also sitting inUnmirroredDeclared, so that file's two population counts move with it (6 entries / 23 keys to 7 / 24; the "no entry in either" population 142 to 141). ⛔UnmirroredDeclaredis shrink-only and was not touched.Two of the 13 declare a second spelling for a slot that already has one —
contentbesidevalue,nodesbesidedata— because that is what the renderers read (schema.content || schema.value,boundData || schema.nodes || schema.data). AGENTS.md #0.1's "one strict contract beats N dialects" argues for retiring one of each pair; that is an ADR-0049 enforce-or-remove question and deliberately not decided here. Each doc comment names which spelling wins.Ablation — direction predicted IN WRITING before the run
Predicted before anything touched disk (mutating the fact, not the assertion): delete the
contentshape entry from theTextSchemamirror, leaving the TS declaration in place..shape: under passthrough an undeclaredcontent: 'hello'is still accepted and still survives, so a suite measuring only acceptance would read this ablation as fully greenzod-mirror-parity.test.ts's compile-time half reddens naminglayout.zod.ts#TextSchema; its runtime census (5 tests) does not move, by that file's documented designObserved, on
065b16b98, all seven as predicted:Mechanics: absolute paths from
git rev-parse --show-toplevel, restore viagit checkout HEAD -- ABSOLUTE_PATH(never bare) undertrap ... EXIT INT TERM, mutation proven by anchored count 1 to 0 and agit hash-objectdiffering from the HEAD blob, restore proven both ways. Resolution path for the ablation: both instruments read SOURCE — the pin file imports'../zod/layout.zod'relatively andtsc -p tsconfig.test.jsoncompilessrc/, sodistis on neither path and no rebuild is part of it.Gates — exit codes captured before any pipe, verdict lines quoted
pnpm --filter @object-ui/types type-checktsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json; lock verdictcommand-exit 0pnpm exec vitest run packages/types/— BEFORE (40c479af2)Test Files 75 passed (75)/Tests 861 passed (861)pnpm exec vitest run packages/types/— AFTER (065b16b98)Test Files 76 passed (76)/Tests 938 passed (938)— +1 file, +77 tests, exactly the new pin file; nothing lostzod-mirror-parityruntime halfTest Files 1 passed (1)/Tests 5 passed (5),PARITY_EXIT=0zod-mirror-paritycompile-time halftype-checkabove (it is atsconfig.test.jsonassertion, not a vitest case)pnpm exec eslint .— plain form, whole repo, at065b16b98node scripts/check-changeset-presence.mjs0— "12 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)"pnpm --filter @object-ui/components type-checkcommand-exit 0— the renderers now type-check against the declared types instead ofany, resolved through the freshly builtdist(packages/components/tsconfig.jsonoverrides the rootpaths, so it reads the built.d.ts, not source)Heavy runs went through the container's shared verify lock; the
--filtervitest forms AGENTS.md guard-refuses were not used.The changeset is
minor(⛔ notmajor) and states the widening in words, including the value-dimension narrowing.A 14th key exists — reported, NOT folded in
The re-derivation ran the opposite way round from the census (enumerate every
schema.KEYNAMEread in each of the 8 renderers, subtract the declared members) and found 4 more genuinely-read undeclared keys the docs-driven census could not see, plus one declared-but-dead key. Per the dispatch order they are reported, not folded in: filed as #6938.CheckboxSchema.wrapperClass(form/checkbox.tsx:33) — the same key as two of the 13, on a third type, undeclared only because the checkbox docs page is a six-line summaryContextMenuSchema.triggerClassName(:87),contentClassName(:88),modal(:91)ContextMenuSchema.childrenis declared required on both faces and read by nothing — the renderer rendersschema.trigger, neverschema.childrenThat sweep covered only these 8 types, so it is a floor, not a census.
Open questions for contract review
CarouselSchema.optsshape. Open record (what shipped here, preserves every working document) vs the docs'{ loop, align }pair (stronger authoring guard, refuses authored embla options that work today). Recommendation: keep it open in this PR and rule on it against real authored data, because narrowing is the irreversible direction.content/value,nodes/data). This PR declares both spellings because both are read. Recommendation: a follow-up ADR-0049 card retires one of each, rather than leaving two dialects declared forever.TreeViewSchema.datais required whilenodesis the spelling the renderer prefers. Anodes-only document is still refused. Recommendation: a separate card, since relaxing a required key is an accept-set change of its own.Generated by Claude Code