feat(types): declare RichtextFieldMetadata for the widget's third registry key - #8373
Draft
claude[bot] wants to merge 3 commits into
Draft
feat(types): declare RichtextFieldMetadata for the widget's third registry key#8373claude[bot] wants to merge 3 commits into
claude[bot] wants to merge 3 commits into
Conversation
…istry key `markdown`, `html` and `richtext` are one widget (objectui#5498). Two of the three carried an exported metadata type; `richtext` carried none, so the runtime served it by structure while an author could not write its metadata under an annotation at all. The only way to write one was `as unknown as MarkdownFieldMetadata` — a deliberate cast in this repo's own pin test that was the gap's sole evidence. `RichtextFieldMetadata` is now exported from `@object-ui/types` and joins the `FieldMetadata` union, so a richtext field's metadata is a typed literal and narrows out of the union on `type`. The cast and its comment are gone, and the surrounding docblock is re-pointed so the gap cannot be rediscovered. The member's shape was derived from what `RichTextField` reads on the `richtext` path, not copied from the two siblings: `type`, `rows`, `placeholder`, `mobile_fullscreen` and `label` (the last three already sit on `BaseFieldMetadata`), with the readonly branch's cell renderers contributing no metadata key. `max_length` is declared on the measurement that `richtext` is symmetric with `markdown`/`html` at the `@objectstack/spec` 17.3.0 authoring boundary, not on the siblings having it. Additive only: nothing is removed or narrowed, `RichTextField` is unchanged, and metadata previously written through a cast keeps compiling. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QtGhnU3WnnWyiWeYQhw2aX
…htext-field-metadata
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
|
…ew union member Comment only — no behaviour, no read site, no signature changes. The paragraph above `richField` said "the third key, `richtext`, has no union member of its own". This branch is what makes that false, so the correction belongs to this diff rather than to a follow-up: the ruling's stated reason for deleting the pin test's comment was that the gap must not stay rediscoverable, and the same sentence sat four lines from where a reader of the widget looks. The cast itself is unchanged and stays correct. It names two of the three members because it never had to discriminate: every key this widget consumes off the carrier is declared on all three, so the two named already admit each read below. The comment now says that outright instead of implying the third key is undeclarable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QtGhnU3WnnWyiWeYQhw2aX
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 Sep 7, 2026
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 free
to 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 #7083
Executes the maintainer's ruling of 2026-09-07 (director seat, decision batch #71, comment 5565745438): the three registry keys one widget serves each get an exported metadata type.
RichtextFieldMetadatais added to@object-ui/types, joins theFieldMetadataunion, and the deliberateas unknown as MarkdownFieldMetadatainpackages/fields/src/widgets/__tests__/RichTextField.rows.test.tsx:50— with its comment at:48— is gone.What was missing
markdown,htmlandrichtextare one widget (#5498). Two of the three carried an exported metadata type;richtextcarried none, so the runtime served it happily by structure while an author could not write its metadata under an annotation at all. The state was neither a member nor a recorded alias — a silent gap whose only evidence was a cast in this repo's own pin test. That asymmetry is why the gap had to be rediscovered to be seen, and #7083 is itself that rediscovery.So the comment goes with the cast, and the surrounding docblock in that file is re-pointed: the next reader should not be able to find the gap again.
The read set was DERIVED, not copied
The ruling's load-bearing sentence is "do not copy 'probably the same'". Every key was read off
packages/fields/src/widgets/RichTextField.tsxon therichtextpath (that file is read-only in this change and is unmodified):RichTextField.tsxtype:176resolveRichTextFieldTypereadsfield.typeand strips afield:prefix; called at:245'richtext'on the new memberrows:291`richField?.rowsmobile_fullscreen:299Boolean(richField?.mobile_fullscreen)BaseFieldMetadata:89placeholder:319`richField?.placeholderlabel:339passed toFullscreenFieldEditorBaseFieldMetadata:31The readonly branch (
:280) handsfieldwhole to theRICH_TEXT_CELL_RENDERERSentry for the type; both renderers inrichTextDisplay.tsxdestructurevalueonly, so the display half contributes no metadata key.typeis also the only thing that differs between the three keys at runtime — it selects the display pipeline (richtextreads through the HTML renderer, #5452) and the format label. Nothing else in the widget branches on it.The diff against the two siblings, and the one key that needed a decision
MarkdownFieldMetadataandHtmlFieldMetadataare byte-identical to each other apart from theirtypeliteral:type+max_length?: number+rows?: numberoverBaseFieldMetadata. So the cross-check produced exactly one open question —max_length, the one key on the siblings thatRichTextFielddoes not read.It is declared on the new member, and on a measurement rather than on the siblings having it. Against the installed
@objectstack/spec17.3.0,FieldSchemaanswers identically for all three of the field types this widget serves:markdownhtmlrichtextrows: 4maxLength: 50(the spec's own spelling)max_length: 50(objectui legacy spelling)Omitting it would have left
richtextthe one key of the three whose ceiling cannot be authored under an annotation — a fresh instance of the very asymmetry the member exists to end. That measurement is pinned in the new test file so the member's docblock cannot rot into a false canonical claim (the #7014 failure mode).The pin has two halves, and both are non-vacuous
packages/fields/src/widgets/__tests__/richtext-field-metadata-7083.test.tsx:as, so TypeScript's excess-property check judges every key, and it is assigned toFieldMetadatato pin the union membership the ruling granted plus the narrowing ontype. Measured, not assumed, that this leg actually runs:packages/fields'stype-checkscript istsc --noEmit && tsc -p tsconfig.test.json, andtsc -p tsconfig.test.json --listFilesnames both this file andRichTextField.rows.test.tsx. On the pre-change tree the file does not compile at all — there is no member to annotate against — which is what makes this leg real rather than decorative. A pin proving only the render would still have passed with the cast in place, i.e. in the state this card ends.rows,placeholder,mobile_fullscreenandlabelare each asserted at the DOM, against a control fixture with all the optional keys absent at once, so a green reads "the widget consumed the declared key" and never "the default happened to match".Scope
Additive only. Nothing is removed or narrowed;
richtextmetadata previously written through a cast keeps compiling;RichTextFielditself is untouched — it already served all three keys, and this change only gives the third one a face. Full downstream sweep:turbo run type-check --filter='...@object-ui/types'— 77/77 tasks green, so widening theFieldMetadataunion breaks no consumer.packages/fieldschanges are test-only, so the changeset grades@object-ui/typesminor and nothing else.Verification
Exit codes captured before any pipe; each verdict read from the gate's own printed line.
turbo run build --filter='@object-ui/fields^...'Tasks: 9 successful, 9 totaltype-check(types + fields)Tasks: 12 successful, 12 totaltype-check --filter='...@object-ui/types'(downstream sweep)Tasks: 77 successful, 77 totalvitest run packages/types/Test Files 141 passed (141)·Tests 2695 passed (2695)vitest run packages/fields/Test Files 138 passed (138)·Tests 2352 passed (2352)lint(types + fields)Tasks: 3 successful, 3 total· 0 errors; the new file contributes 0 diagnosticslint:coveragetype-check:coveragechangeset:checkcheck:control-bytescheck:self-importcheck:phantom-depscheck:spec-symbolscheck:spec-floorscheck:designer-field-key-paritycheck:element-data-source-declarationcheck:sdui-registration-pinsAll 16 registration(s) a sideEffects array promises are present in the built console (518 chunks weighed…)check:handler-key-readscheck:doc-fencescheck:doc-typescheck:doc-snippetsSemantic phase: 632 of 632 block(s) judged, 0 failedcheck:readme-exportsOK … 532 of them self-imports judged (532 real, 0 wrong-path, 0 fabricated) … 0 unbuiltcheck:doc-example-readerscheck:unreferenced-sourcescheck:comment-mask-corpuscheck:vi-mock-specifierscheck:side-effects-arraycheck:esm-specifiersBeyond the gates, a control-byte self-scan over the five touched paths (
grep -naPover the C0 range plus DEL) found none.Every gate the dispatch named is measured; none is left declared-to-CI. Three of them (
check:readme-exports,check:doc-snippets,check:sdui-registration-pins) have environment preconditions — built artifacts — and each was rebuilt until its precondition held rather than accepted at its first exit code.Two of those readings were false on the first attempt and are reported as re-measured, not as first answers.
check:readme-exportsfirst exited 1 naming 8 unjudged self-imports inpackages/cliandpackages/plugin-ai— both simply unbuilt in this worktree, neither touched by this diff — andcheck:doc-snippetsfirst exited 1 onpackages/plugin-chatbot/README.md:371, which is a staledist: sibling #8341 landed that README inmainbetween this branch's base and the merge below, while the built artifact it compiles against predated it. Rebuilding each package and re-running is what produced the zeroes above; neither red was about this change, and neither was accepted as a reading until the artifact under it was current.验收备注
Observations found while deriving. Neither is filed as a card; both are recorded here rather than fixed, because the dispatch's file surface makes
RichTextField.tsxread-only and the second one is another card's declared surface.RichTextField.tsx:283-289now carries a stale claim. Its comment says "the third key,richtext, has no union member of its own and structurally matches the same three optional reads below". After this PR that sentence is false, and it sits exactly where the next reader would look — the same rediscovery loop the ruling asked to close by removing the test's comment. It is a two-line prose correction inside a file this dispatch declared READ ONLY, so it is deliberately not touched here. Recommend folding it into whichever change next opens that file, or a one-line follow-up.rowsdocblocks onMarkdownFieldMetadataandHtmlFieldMetadatastill describe the@objectstack/spec17.2.0 boundary (whererowswas refused by name); at 17.3.0 it is admitted. That prose is already When the @objectstack/spec pin moves to 17.3: PR #7510 / #7520's comment "repairs" become FALSE —rowsandoptions[].descriptionare now declared keys #7635's declared surface — recorded inpackages/types/src/__tests__/select-option-spec-extension-7014.test.ts— so this PR states the 17.3.0 reading forrichtextonly and does not correct the neighbours, to avoid the two halves being repaired twice or, worse, once.Review
Clause-② applies (a new published type-face member), so
needs:contract-reviewis hung on this PR as well as on the card. This PR stays draft, is not enqueued and has no auto-merge armed; parking green is the intended end state until aCONTRACT_REVIEW_TIERreviewer clears both carriers. Neither carrier is cleared by this seat.Authored by the
domain:spec@ objectui execution seat, sessionhttps://claude.ai/code/session_01QtGhnU3WnnWyiWeYQhw2aX— kept in prose as a code span because aPATCHto this body downgrades a link-form attribution footer and drops the session reference.🤖 Generated with Claude Code
https://claude.ai/code/session_01QtGhnU3WnnWyiWeYQhw2aX
Generated by Claude Code