Uh oh!
There was an error while loading. Please reload this page.
feat(types)!: retire ComponentInput's four inert constraint keys, and report the inputType fork - #6949
Merged
Conversation
`min`, `max`, `step` and `placeholder` were declared on `ComponentInput` and read by nothing, on either the consumption or the publication path. The manifest serializer forwards exactly six keys per input — `name`, `type`, `required`, `enum`, `binding`, `description` — so an authored value could not reach the published `sdui.manifest.json` even in principle. Retired as ADR-0049 tombstones rather than deletions: `?: never` on the interface (a `tsc` error at the authoring site) and `retirementTombstone()` on the Zod mirror (a parse refusal carrying its own migration note). A deletion would have been stripped silently by the non-strict mirror — one silent no-op traded for another. `inputType` is deliberately NOT retired: `plugin-markdown` authors it, which makes it declared-and-dropped, a different defect needing a ruling. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
…lock The block enumerated five keys `ComponentInput` carries that `WidgetInput` does not. Four of them are now tombstones, so "five keys this face declines to copy" became "one live key plus four unwritable ones" — stated where the next reader of that divergence will look. 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
|
os-sam
marked this pull request as ready for review
August 31, 2026 03:28
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Sep 1, 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 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.
Refs #5905.⚠️
Refs, not a closing keyword — the card asked for FIVE keys and thisretires FOUR. The fifth,
inputType, hit the fork condition triage set, and the cardstays open on that fork.
needs:contract-reviewtier: this changes members on a published type(
@object-ui/types→ComponentInput). ⛔ Draft on purpose — not ready, not enqueued, noauto-merge.
Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
1. The fork, first — it is the point of the card, not a footnote
Triage: "If any registration in the repo AUTHORS one of the five (declared-and-dropped is
a different defect), stop and report the fork with the site list."
One site authors one of the five. The site list is one line long:
inputTypepackages/plugin-markdown/src/index.tsx:60inputs: [{ name: 'content', type: 'string', label: 'Markdown Content', required: true, inputType: 'textarea' }]It is a real
ComponentInput:ComponentRegistry.register(type, component, meta)takesmetaasComponentMeta(packages/core/src/registry/Registry.ts:352), whoseinputsisComponentInput[]. It is also pinned —packages/plugin-markdown/src/index.test.ts:51asserts
expect(contentInput?.inputType).toBe('textarea').So
inputTypeis declared-and-DROPPED, not declared-and-unread: something writes it,and the serializer discards it. That is a different defect and a different remedy, and
retiring it here would turn one registration's silent no-op into a build failure without
anyone deciding what that registration should say instead.
inputTypeis left live andwritable, with the fork recorded in its doc block, in the Zod mirror, and in the pin
test — so closing it later is a deliberate edit, not a drive-by.
The other four —
min,max,step,placeholder— have zero authoring sites, andare retired here.
2. What was measured, with its instrument and its bound
Pathspec bound: every tracked file at the merge-base
b03ba3ad5— 5,869 files, alltop-level directories,
packages/andapps/andexamples/ande2e/andcontent/and
skills/andscripts/included. Not a package subset.Instrument (structural, not a bare grep): bracket-match every
inputs:array in everytracked
.ts/.tsx/.js/.jsx/.mjs/.cjs/.json/.md/.mdxfile, then collect the top-level keysof the object literals inside — i.e. the
ComponentInput's own keys. 219 regionsscanned.
The zero comes with a HOT CONTROL, from the same pass over the same regions (not
merely repo-wide):
inputTypenamemintypemaxdescriptionstepenumplaceholderrequiredThe instrument was demonstrably not blind: it found the one
inputTypesite and fourdigits of control hits in the same regions.
Blind spot found and closed by hand. The
inputs:scan cannot see aComponentInput[]built as a named constant. All four such constants were read individually — the same
retired-five / forwarded-six pass inside each — and all four are clean:
PAGE_CONTAINER_INPUTS(packages/components/src/renderers/layout/containers.tsx:85),CHATTER_INPUTS(packages/plugin-detail/src/index.tsx:643),GRID_QUERY_INPUTS(
packages/plugin-grid/src/index.tsx:215) andELEMENT_DATA_SOURCE_INPUT(
packages/core/src/data-scope/element-data-source.ts:327).placeholderfalse positives, rejected BY HAND — and that rejection is a finding aboutthe instrument, not noise. A plain
placeholder:grep over the same file set returns 26hits and none of them is a
ComponentInputkey:packages/vscode-extension/src/providers/PreviewProvider.ts:180,193— writesinput.placeholder/textarea.placeholderon a DOM element, fromBaseSchema.placeholder. Different type on both ends.packages/app-shell/src/views/metadata-admin/previews/block-config.ts— an inspectorfield spec whose own key is named
placeholder(aPlaceholderSpec). Different type.combobox,command,date-picker,select,textarea,and three in
plugin-chatbot) writeplaceholderindefaultProps, the element'sown prop — while their
inputs:array carries{ name: 'placeholder', … }, i.e.placeholderas the value ofname, never as aComponentInputkey.plugin-designer/ObjectManager/plugin-view/form.tsxhits are form fielddescriptors (
type: 'text',type: 'input'— neither is aComponentInputControlType).The publication path, re-read on the merge-base rather than trusted from the card.
packages/sdui-parser/src/index.ts:153-160forwards exactly six keys per input:None of the five is in it, so a value authored here could not reach the published
sdui.manifest.jsoneven in principle.Reads: none. A member-access census across the 71 files that consume
.inputsreturnsnothing for
min/max/step/placeholderon aComponentInputvalue (the hits arei18n key strings,
sim.step(), andelement:text_input.placeholder, which is a differentsurface). Same pass, same file set, control:
.name188,.type302,.description68,.enum28,.required23.3.⚠️ The half that is NOT measurable from here — stated, not papered over
The in-repo zero is all that was measured. Whether anything OUTSIDE this repository
writes these keys is out of reach from here — the same limit objectui#5674 recorded for
PluginComponentInput, and the card names it explicitly as the thing that decides between"remove" and "leave as published surface". Nothing in this PR establishes that no external
author writes them, and no sentence here should be read as claiming it.
That unmeasurable half is exactly why this is a tombstone and not a deletion:
ComponentInputSchemais a non-strictz.object, so a deleted key would be silentlystripped — one silent no-op traded for another. The tombstone converts an out-of-repo
write from a silent drop into a NAMED REFUSAL that carries its own remedy.
4.⚠️ This is NOT a verdict that constraint slots were a mistake
The neighbouring
typefield carries a maintainer ruling of 2026-08-17, quoted inbase.ts:min/max/stepread exactly like the slots that ruling declined to add. What isretired here is this inert spelling of them, not the idea — a future reader must not read
this PR as "these keys were a mistake". The ruling's own reopen condition still stands, and
the reopen route is a designed one, not a revert of this change.
5. What changed
packages/types/src/base.ts—min/max/step/placeholderbecome?: nevertombstones (this package's convention:crud.tsconfirm,StaticTableColumn), each with its own guidance and@deprecated.inputTypekeeps itstype and gains the fork note.
packages/types/src/zod/base.zod.ts— the same four becomeretirementTombstone(guidance)fromzod/tombstone.zod.ts.z.never().optional().describe(...)spelling the retirement playbook and the finding(types):
MobileComponentConfigis now a fully inert published type — zero mount points, zero readers #5942 precedent would have produced: bothpredate PR fix(types): carry the tombstone remediation text into the refusal message #6930 (card finding(types): the ADR-0049
z.never()retirement tombstones refuse by name but their.describe()remediation text never reaches the author — runtime message is zod's generic "expected never" #6105). The helper carries the guidance string once into bothchannels —
z.never({ error })for the runtime message and.describe()forJSON-Schema/docs — so they cannot drift.
.describe()survives, and therefineroute(which reports issue code
custominstead ofinvalid_type) is not used.packages/types/src/__tests__/component-input-retired-constraint-keys.test.ts— new pin.packages/types/src/widget.ts—WidgetInput's divergence block enumerated "five keysComponentInputcarries that this face does not". Four of them are now unwritable, sothe block now says one live key plus four tombstones. Same declaration family, same
package, same gate; its existing pin
(
widget-input-control-vocabulary.test.ts, which requires the block to name each of thefive) still passes.
.changeset/5905-componentinput-retire-constraint-keys.md—minor, states the removalin words with a FROM → TO line per key.
Considered and deliberately not touched:
packages/core/src/registry/Registry.ts'sdoc block, which describes the same four keys in the past tense as objectui#4972's
history ("were missing from the copy every registration actually imports"). It stays true
as history, and editing it would pull a second package into this changeset.
Docs and skills need no edit: no
content/docspage and no published skills file teachesthese four on
ComponentInput— the skill's ownComponentInputtype block(
skills/objectui/guides/plugin-development.md:87) already lists eight keys and none of thefive. This diff does not touch the published skills directory at all.
6.⚠️ The emitted
.d.ts, measured on BOTH sides — what the contract reviewer needsBuilt with
pnpm --filter @object-ui/types buildon each side, then diffed.packages/types/dist/base.d.ts—interface ComponentInput(comments stripped):Exact statement of what disappears:no member is removed from the emitted
.d.ts.The four members stay declared and their types change from
number/number/number/stringtonever. That is the tombstone contract: the key stays visible so anauthor who writes it meets a message, and unwritable so writing it fails. Member count is
13 before and 13 after.
packages/types/dist/zod/base.zod.d.ts— the same four, before → after.body's sanitizer eats tag-shaped fragments, and on the first publish it ate exactly this
table — both columns came back reading
z.ZodOptionaland the diff was thereforemeaningless. Read "OPT of X" as
z.ZodOptionalparameterised byX.minz.ZodNumberz.ZodNevermaxz.ZodNumberz.ZodNeverstepz.ZodNumberz.ZodNeverplaceholderz.ZodStringz.ZodNeverThat shape is inlined three times in that one file —
ComponentInputSchema,ComponentMetaSchemaandComponentConfigSchemaall carry it.Which published subpaths carry the change (from
package.jsonexports):@object-ui/types/basedist/base.d.ts@object-ui/typesdist/index.d.tsComponentInput)@object-ui/typesdist/index.d.tsPluginComponentInput(viadist/plugin-scope.d.ts)@object-ui/types/zoddist/zod/index.zod.d.ts→dist/zod/base.zod.d.tsComponentInputSchema,ComponentMetaSchema,ComponentConfigSchemadist/widget.d.tsmentionsComponentInputonly in prose and importsComponentInputControlType;WidgetInput's own eight-key shape is unchanged.7. The refusal is real — before/after
safeParse, with a control both runsSame probe, same document shape, run against the mirror before and after the change.
Control document:
{ name: 'content', type: 'string', label: …, required: true }.min: 0success=true, value KEPTsuccess=false, path["min"], codeinvalid_typemax: 100success=true, value KEPTsuccess=false, path["max"], codeinvalid_typestep: 1success=true, value KEPTsuccess=false, path["step"], codeinvalid_typeplaceholder: 'Type here…'success=true, value KEPTsuccess=false, path["placeholder"], codeinvalid_typeinputType: 'textarea'(the fork)success=true, value KEPTsuccess=true, value KEPT — unchanged on purposedescription: 'Help text'success=truesuccess=truesuccess=truesuccess=truemessageper key, AFTER (this is the.describe()text too — one string, both channels):maxandstepare the same sentence with their own key.placeholder's ends insteadwith: "Delete the key; put the hint in
description, which IS published.BaseSchema.placeholder, the node-level prop, is a DIFFERENT key and is unaffected." —because
BaseSchema.placeholder(zod/base.zod.ts:105) is a live key an author must notthink was retired.
The refusal is a narrowing that speaks, not a silent narrowing: the pin test also
measures the contrast in the same run — a genuinely undeclared key
(
notAKeyAtAll: 'anything') still parsessuccess=trueand is silently stripped, which isprecisely what deleting these four members would have bought.
8. Ablation — direction predicted in writing BEFORE the run
Predicted, before running: replace
min: retirementTombstone(…)with its exactpre-change spelling
min: z.number().optional().describe('Minimum value')— mutating thefact, never an assertion — and the pin file goes RED and NARROWLY: exactly two
assertions, "refuses
min…" (expected true to be false) and "the four stay in themirror's shape …" (describe reverts to
'Minimum value'), withmax/step/placeholder, the live-input control, theinputTypefork pin and the strip contrast allstaying GREEN.
Rebuild question, answered rather than assumed: no build is needed for this leg, and
the reason is the resolution path, not the suite's name — the pin test imports
../zod/base.zod, a relative source specifier (asserted mechanically in the script), sovitest loads the mutated
srcdirectly and a staledistcannot hide the mutation.Observed — matches the prediction:
(The one place the prediction was off: it said "2 failed | 8 passed" — the file has 11
tests, not 10, so 9 passed. The failure count and both named assertions matched exactly.)
Mutation proven on disk — anchored counts in both directions plus hashes, never an
editor's exit code: removed text
min: retirementTombstone(count 0, injected textcount 1, remaining tombstone calls 4 (1 import + 3 keys),
git hash-objectc82523ed…→622c6d39….Restore proven BOTH ways, under
trap … EXIT INT TERM, absolute paths fromgit rev-parse --show-toplevel, withgit checkout HEAD -- ABSOLUTE_PATH(never the bareform, which reads from the index):
git diff HEADempty and each file'sgit hash-objectequal to itsHEADblob, for all four touched paths. Then apost-restore control: the same file green again,
Tests 11 passed (11).9. Gates — exit codes captured before any pipe, on the final commit
Union re-run at
33846a77d, worktree clean, after the last commit:pnpm --filter @object-ui/types buildtscBUILD_EXIT=0pnpm --filter @object-ui/types type-check(hyphenated)tsc --noEmitthentsc -p tsconfig.examples.jsonthentsc -p tsconfig.test.jsonTYPECHECK_EXIT=0pnpm exec vitest run packages/types/(root form)Test Files 76 passed (76)·Tests 872 passed (872)0pnpm exec vitest run packages/core/src/registry/Test Files 7 passed (7)·Tests 111 passed (111)0pnpm exec vitest run packages/sdui-parser/Test Files 11 passed (11)·Tests 147 passed (147)0pnpm exec eslint .(plain form, no--no-inline-config)✖ 11516 problems (0 errors, 11516 warnings)ESLINT_EXIT=0node scripts/check-changeset-presence.mjs✅ 4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)0zod-mirror-parityTest Files 1 passed (1)·Tests 5 passed (5)0vitest file/test counts, before → after (the wrong-cwd false green this repo has been
bitten by would report
apps/console's 22 files instead):packages/types)Sanity-checked against the target's own count:
git ls-files 'packages/types/**'matching*.test.ts(x)is 76 — equal to what vitest reported — and 75 atb03ba3ad5. The+1 file / +11 tests are exactly the new pin file.
zod-mirror-paritydeserves its own note since this removes declarations it pairs: itcompares the mirror's
.shapeagainst the declaration over the INTERSECTION of the mirror'skeys and the declaration's, so the four stay in the mirror and leave the declaration's
writable set without entering any ledger.
base.zod.ts#ComponentInputSchemahas noKnownDriftand noUnmirroredDeclaredentry before or after — same precedent asDashboardComponentSchema.aria(objectui#5855).eslint reports 0 errors; the 11,516 warnings are the repo's pre-existing
no-explicit-anypopulation. The four files this PR touches contribute zero newfindings (the
base.ts/base.zod.tswarnings are on untouched lines).10. What is NOT claimed
inputTypeshould stay. It needs a ruling; this PR does not make it.ComponentInputare wrong. See §4.check:*set — CI ownsthose and runs them exactly once on this branch.