feat(types)!: retire ComponentInput's four inert constraint keys, and report the inputType fork - #6949

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-5905-componentinput-retire-five-keys
Aug 31, 2026
Merged

feat(types)!: retire ComponentInput's four inert constraint keys, and report the inputType fork#6949
os-sam merged 2 commits into
mainfrom
claude/issue-5905-componentinput-retire-five-keys

Conversation

@os-sam

@os-samos-sam commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Refs #5905. ⚠️Refs, not a closing keyword — the card asked for FIVE keys and this
retires FOUR.
The fifth, inputType, hit the fork condition triage set, and the card
stays open on that fork.

needs:contract-review tier: this changes members on a published type
(@object-ui/typesComponentInput). ⛔ Draft on purpose — not ready, not enqueued, no
auto-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:

keysitewhat it writes
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) takes
meta as ComponentMeta (packages/core/src/registry/Registry.ts:352), whose inputs is
ComponentInput[]. It is also pinnedpackages/plugin-markdown/src/index.test.ts:51
asserts expect(contentInput?.inputType).toBe('textarea').

So inputType is 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. inputType is left live and
writable
, 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, and
are 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, all
top-level directories, packages/ and apps/ and examples/ and e2e/ and content/
and skills/ and scripts/ included.
Not a package subset.

Instrument (structural, not a bare grep): bracket-match every inputs: array in every
tracked .ts/.tsx/.js/.jsx/.mjs/.cjs/.json/.md/.mdx file, then collect the top-level keys
of the object literals inside — i.e. the ComponentInput's own keys. 219 regions
scanned.

The zero comes with a HOT CONTROL, from the same pass over the same regions (not
merely repo-wide):

retired candidatesitescontrol: a key the serializer DOES forwardsites
inputType1name926
min0type926
max0description161
step0enum114
placeholder0required87

The instrument was demonstrably not blind: it found the one inputType site and four
digits of control hits in the same regions.

Blind spot found and closed by hand. The inputs: scan cannot see a ComponentInput[]
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) and ELEMENT_DATA_SOURCE_INPUT
(packages/core/src/data-scope/element-data-source.ts:327).

placeholder false positives, rejected BY HAND — and that rejection is a finding about
the instrument, not noise.
A plain placeholder: grep over the same file set returns 26
hits and none of them is a ComponentInput key:

  • packages/vscode-extension/src/providers/PreviewProvider.ts:180,193 — writes
    input.placeholder / textarea.placeholder on a DOM element, from
    BaseSchema.placeholder. Different type on both ends.
  • packages/app-shell/src/views/metadata-admin/previews/block-config.ts — an inspector
    field spec whose own key is named placeholder
    (a PlaceholderSpec). Different type.
  • Eight renderer registrations (combobox, command, date-picker, select, textarea,
    and three in plugin-chatbot) write placeholder in defaultProps, the element's
    own prop — while their inputs: array carries { name: 'placeholder', … }, i.e.
    placeholder as the value of name, never as a ComponentInput key.
  • plugin-designer / ObjectManager / plugin-view / form.tsx hits are form field
    descriptors (type: 'text', type: 'input' — neither is a ComponentInputControlType).

The publication path, re-read on the merge-base rather than trusted from the card.
packages/sdui-parser/src/index.ts:153-160 forwards exactly six keys per input:

inputs: (c.inputs??[]).map((i)=>({name: i.name,type: canonicalizeInputType(i.type),required: i.required,enum: i.enum,binding: i.binding,description: i.description,})),

None of the five is in it, so a value authored here could not reach the published
sdui.manifest.json even in principle.

Reads: none. A member-access census across the 71 files that consume .inputs returns
nothing for min / max / step / placeholder on a ComponentInput value (the hits are
i18n key strings, sim.step(), and element:text_input.placeholder, which is a different
surface). Same pass, same file set, control: .name 188, .type 302, .description 68,
.enum 28, .required 23.

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:
ComponentInputSchema is a non-strict z.object, so a deleted key would be silently
stripped
— 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 type field carries a maintainer ruling of 2026-08-17, quoted in
base.ts:

Ruled (maintainer, 2026-08-17): the coarse arm plus description IS the publication
face's expression ceiling today, and SPEC IS THE SOLE JUDGE OF VALUES.

Two directions were deferred, not rejected on merit: giving ComponentInput real
constraint slots (two sources of truth, free to drift), and binding checkType to spec's
Zod member when a ComponentPropsMap entry exists (one truth, but couples sdui-parser
to spec). The ruling names the reopen condition: a measured case of an author — human
or agent — shipping a spec-rejected value that objectui's silence let through.

min / max / step read exactly like the slots that ruling declined to add. What is
retired 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

Considered and deliberately not touched: packages/core/src/registry/Registry.ts's
doc 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/docs page and no published skills file teaches
these four on ComponentInput — the skill's own ComponentInput type block
(skills/objectui/guides/plugin-development.md:87) already lists eight keys and none of the
five. This diff does not touch the published skills directory at all.

6. ⚠️ The emitted .d.ts, measured on BOTH sides — what the contract reviewer needs

Built with pnpm --filter @object-ui/types build on each side, then diffed.

packages/types/dist/base.d.tsinterface ComponentInput (comments stripped):

 description?: string;
advanced?: boolean;
inputType?: string;
- min?: number;- max?: number;- step?: number;- placeholder?: string;+ min?: never;+ max?: never;+ step?: never;+ placeholder?: never;
}

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 / string to never. That is the tombstone contract: the key stays visible so an
author 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.
⚠️ The angle-bracket generic spelling is written out in words below on purpose: this
body's sanitizer eats tag-shaped fragments, and on the first publish it ate exactly this
table — both columns came back reading z.ZodOptional and the diff was therefore
meaningless. Read "OPT of X" as z.ZodOptional parameterised by X.

memberBEFOREAFTER
minOPT of z.ZodNumberOPT of z.ZodNever
maxOPT of z.ZodNumberOPT of z.ZodNever
stepOPT of z.ZodNumberOPT of z.ZodNever
placeholderOPT of z.ZodStringOPT of z.ZodNever

That shape is inlined three times in that one file — ComponentInputSchema,
ComponentMetaSchema and ComponentConfigSchema all carry it.

Which published subpaths carry the change (from package.jsonexports):

subpathfilehow it carries it
@object-ui/types/basedist/base.d.tsthe declaration itself
@object-ui/typesdist/index.d.tsre-exports the type by name (ComponentInput)
@object-ui/typesdist/index.d.tsre-exports it again as the deprecated alias PluginComponentInput (via dist/plugin-scope.d.ts)
@object-ui/types/zoddist/zod/index.zod.d.tsdist/zod/base.zod.d.tsComponentInputSchema, ComponentMetaSchema, ComponentConfigSchema

dist/widget.d.ts mentions ComponentInput only in prose and imports
ComponentInputControlType; WidgetInput's own eight-key shape is unchanged.

7. The refusal is real — before/after safeParse, with a control both runs

Same probe, same document shape, run against the mirror before and after the change.
Control document: { name: 'content', type: 'string', label: …, required: true }.

key writtenBEFOREAFTER
min: 0success=true, value KEPTsuccess=false, path ["min"], code invalid_type
max: 100success=true, value KEPTsuccess=false, path ["max"], code invalid_type
step: 1success=true, value KEPTsuccess=false, path ["step"], code invalid_type
placeholder: 'Type here…'success=true, value KEPTsuccess=false, path ["placeholder"], code invalid_type
inputType: 'textarea' (the fork)success=true, value KEPTsuccess=true, value KEPT — unchanged on purpose
CONTROL description: 'Help text'success=truesuccess=true
CONTROL bare documentsuccess=truesuccess=true

message per key, AFTER (this is the .describe() text too — one string, both channels):

RETIRED (objectui#5905) — ComponentInput.min was never read, and never published: the
manifest serializer forwards name/type/required/enum/binding/description and
this is not one of them, so an authored value was silently dropped. Delete the key; spell
the numeric domain out in description, which IS published.

max and step are the same sentence with their own key. placeholder's ends instead
with: "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 not
think 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 parses success=true and is silently stripped, which is
precisely 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 exact
pre-change spelling min: z.number().optional().describe('Minimum value') — mutating the
fact, 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 the
mirror's shape …" (describe reverts to 'Minimum value'), with max / step /
placeholder, the live-input control, the inputType fork pin and the strip contrast all
staying 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), so
vitest loads the mutated src directly and a stale dist cannot hide the mutation.

Observed — matches the prediction:

 × refuses `min`, names it in the path, and answers with its own guidance
× the four stay in the mirror's shape — a tombstone is DECLARED, just unwritable
AssertionError: min: expected true to be false // Object.is equality
AssertionError: expected 'Minimum value' to contain 'RETIRED (objectui#5905)'
Test Files 1 failed (1)
Tests 2 failed | 9 passed (11)

(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 text
count 1, remaining tombstone calls 4 (1 import + 3 keys), git hash-object
c82523ed…622c6d39….

Restore proven BOTH ways, under trap … EXIT INT TERM, absolute paths from
git rev-parse --show-toplevel, with git checkout HEAD -- ABSOLUTE_PATH (never the bare
form, which reads from the index): git diff HEAD empty and each file's
git hash-object equal to its HEAD blob, for all four touched paths. Then a
post-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:

gateverdict lineexit
pnpm --filter @object-ui/types buildtscBUILD_EXIT=0
pnpm --filter @object-ui/types type-check (hyphenated)tsc --noEmit then tsc -p tsconfig.examples.json then tsc -p tsconfig.test.jsonTYPECHECK_EXIT=0
pnpm exec vitest run packages/types/ (root form)Test Files 76 passed (76) · Tests 872 passed (872)0
pnpm exec vitest run packages/core/src/registry/Test Files 7 passed (7) · Tests 111 passed (111)0
pnpm exec vitest run packages/sdui-parser/Test Files 11 passed (11) · Tests 147 passed (147)0
pnpm exec eslint . (plain form, no --no-inline-config)✖ 11516 problems (0 errors, 11516 warnings)ESLINT_EXIT=0
node scripts/check-changeset-presence.mjs✅ 4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)0
zod-mirror-parityTest Files 1 passed (1) · Tests 5 passed (5)0

vitest file/test counts, before → after (the wrong-cwd false green this repo has been
bitten by would report apps/console's 22 files instead):

Test FilesTests
BEFORE (merge-base state of packages/types)75861
AFTER76872
delta+1+11

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 at b03ba3ad5. The
+1 file / +11 tests are exactly the new pin file.

zod-mirror-parity deserves its own note since this removes declarations it pairs: it
compares the mirror's .shape against the declaration over the INTERSECTION of the mirror's
keys 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#ComponentInputSchema has no
KnownDrift and no UnmirroredDeclared entry before or after — same precedent as
DashboardComponentSchema.aria (objectui#5855).

eslint reports 0 errors; the 11,516 warnings are the repo's pre-existing
no-explicit-any population. The four files this PR touches contribute zero new
findings
(the base.ts / base.zod.ts warnings are on untouched lines).

10. What is NOT claimed

  • Not claimed: that nothing outside this repository writes these keys. Not measurable here.
  • Not claimed: that inputType should stay. It needs a ruling; this PR does not make it.
  • Not claimed: that constraint slots on ComponentInput are wrong. See §4.
  • Not run locally: the full repo test farm and the rest of the check:* set — CI owns
    those and runs them exactly once on this branch.

os-samand others added 2 commits August 31, 2026 02:35
`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
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3179.9 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-DkRl-aik.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 03:28
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 453dbaaAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-5905-componentinput-retire-five-keys branch August 31, 2026 03:42
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@os-sam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(types)!: retire ComponentInput's four inert constraint keys, and report the inputType fork - #6949

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-5905-componentinput-retire-five-keys
Aug 31, 2026
Merged

feat(types)!: retire ComponentInput's four inert constraint keys, and report the inputType fork#6949
os-sam merged 2 commits into
mainfrom
claude/issue-5905-componentinput-retire-five-keys

Conversation

@os-sam

@os-samos-sam commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Refs #5905. ⚠️Refs, not a closing keyword — the card asked for FIVE keys and this
retires FOUR.
The fifth, inputType, hit the fork condition triage set, and the card
stays open on that fork.

needs:contract-review tier: this changes members on a published type
(@object-ui/typesComponentInput). ⛔ Draft on purpose — not ready, not enqueued, no
auto-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:

keysitewhat it writes
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) takes
meta as ComponentMeta (packages/core/src/registry/Registry.ts:352), whose inputs is
ComponentInput[]. It is also pinnedpackages/plugin-markdown/src/index.test.ts:51
asserts expect(contentInput?.inputType).toBe('textarea').

So inputType is 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. inputType is left live and
writable
, 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, and
are 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, all
top-level directories, packages/ and apps/ and examples/ and e2e/ and content/
and skills/ and scripts/ included.
Not a package subset.

Instrument (structural, not a bare grep): bracket-match every inputs: array in every
tracked .ts/.tsx/.js/.jsx/.mjs/.cjs/.json/.md/.mdx file, then collect the top-level keys
of the object literals inside — i.e. the ComponentInput's own keys. 219 regions
scanned.

The zero comes with a HOT CONTROL, from the same pass over the same regions (not
merely repo-wide):

retired candidatesitescontrol: a key the serializer DOES forwardsites
inputType1name926
min0type926
max0description161
step0enum114
placeholder0required87

The instrument was demonstrably not blind: it found the one inputType site and four
digits of control hits in the same regions.

Blind spot found and closed by hand. The inputs: scan cannot see a ComponentInput[]
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) and ELEMENT_DATA_SOURCE_INPUT
(packages/core/src/data-scope/element-data-source.ts:327).

placeholder false positives, rejected BY HAND — and that rejection is a finding about
the instrument, not noise.
A plain placeholder: grep over the same file set returns 26
hits and none of them is a ComponentInput key:

  • packages/vscode-extension/src/providers/PreviewProvider.ts:180,193 — writes
    input.placeholder / textarea.placeholder on a DOM element, from
    BaseSchema.placeholder. Different type on both ends.
  • packages/app-shell/src/views/metadata-admin/previews/block-config.ts — an inspector
    field spec whose own key is named placeholder
    (a PlaceholderSpec). Different type.
  • Eight renderer registrations (combobox, command, date-picker, select, textarea,
    and three in plugin-chatbot) write placeholder in defaultProps, the element's
    own prop — while their inputs: array carries { name: 'placeholder', … }, i.e.
    placeholder as the value of name, never as a ComponentInput key.
  • plugin-designer / ObjectManager / plugin-view / form.tsx hits are form field
    descriptors (type: 'text', type: 'input' — neither is a ComponentInputControlType).

The publication path, re-read on the merge-base rather than trusted from the card.
packages/sdui-parser/src/index.ts:153-160 forwards exactly six keys per input:

inputs: (c.inputs??[]).map((i)=>({name: i.name,type: canonicalizeInputType(i.type),required: i.required,enum: i.enum,binding: i.binding,description: i.description,})),

None of the five is in it, so a value authored here could not reach the published
sdui.manifest.json even in principle.

Reads: none. A member-access census across the 71 files that consume .inputs returns
nothing for min / max / step / placeholder on a ComponentInput value (the hits are
i18n key strings, sim.step(), and element:text_input.placeholder, which is a different
surface). Same pass, same file set, control: .name 188, .type 302, .description 68,
.enum 28, .required 23.

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:
ComponentInputSchema is a non-strict z.object, so a deleted key would be silently
stripped
— 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 type field carries a maintainer ruling of 2026-08-17, quoted in
base.ts:

Ruled (maintainer, 2026-08-17): the coarse arm plus description IS the publication
face's expression ceiling today, and SPEC IS THE SOLE JUDGE OF VALUES.

Two directions were deferred, not rejected on merit: giving ComponentInput real
constraint slots (two sources of truth, free to drift), and binding checkType to spec's
Zod member when a ComponentPropsMap entry exists (one truth, but couples sdui-parser
to spec). The ruling names the reopen condition: a measured case of an author — human
or agent — shipping a spec-rejected value that objectui's silence let through.

min / max / step read exactly like the slots that ruling declined to add. What is
retired 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

Considered and deliberately not touched: packages/core/src/registry/Registry.ts's
doc 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/docs page and no published skills file teaches
these four on ComponentInput — the skill's own ComponentInput type block
(skills/objectui/guides/plugin-development.md:87) already lists eight keys and none of the
five. This diff does not touch the published skills directory at all.

6. ⚠️ The emitted .d.ts, measured on BOTH sides — what the contract reviewer needs

Built with pnpm --filter @object-ui/types build on each side, then diffed.

packages/types/dist/base.d.tsinterface ComponentInput (comments stripped):

 description?: string;
advanced?: boolean;
inputType?: string;
- min?: number;- max?: number;- step?: number;- placeholder?: string;+ min?: never;+ max?: never;+ step?: never;+ placeholder?: never;
}

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 / string to never. That is the tombstone contract: the key stays visible so an
author 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.
⚠️ The angle-bracket generic spelling is written out in words below on purpose: this
body's sanitizer eats tag-shaped fragments, and on the first publish it ate exactly this
table — both columns came back reading z.ZodOptional and the diff was therefore
meaningless. Read "OPT of X" as z.ZodOptional parameterised by X.

memberBEFOREAFTER
minOPT of z.ZodNumberOPT of z.ZodNever
maxOPT of z.ZodNumberOPT of z.ZodNever
stepOPT of z.ZodNumberOPT of z.ZodNever
placeholderOPT of z.ZodStringOPT of z.ZodNever

That shape is inlined three times in that one file — ComponentInputSchema,
ComponentMetaSchema and ComponentConfigSchema all carry it.

Which published subpaths carry the change (from package.jsonexports):

subpathfilehow it carries it
@object-ui/types/basedist/base.d.tsthe declaration itself
@object-ui/typesdist/index.d.tsre-exports the type by name (ComponentInput)
@object-ui/typesdist/index.d.tsre-exports it again as the deprecated alias PluginComponentInput (via dist/plugin-scope.d.ts)
@object-ui/types/zoddist/zod/index.zod.d.tsdist/zod/base.zod.d.tsComponentInputSchema, ComponentMetaSchema, ComponentConfigSchema

dist/widget.d.ts mentions ComponentInput only in prose and imports
ComponentInputControlType; WidgetInput's own eight-key shape is unchanged.

7. The refusal is real — before/after safeParse, with a control both runs

Same probe, same document shape, run against the mirror before and after the change.
Control document: { name: 'content', type: 'string', label: …, required: true }.

key writtenBEFOREAFTER
min: 0success=true, value KEPTsuccess=false, path ["min"], code invalid_type
max: 100success=true, value KEPTsuccess=false, path ["max"], code invalid_type
step: 1success=true, value KEPTsuccess=false, path ["step"], code invalid_type
placeholder: 'Type here…'success=true, value KEPTsuccess=false, path ["placeholder"], code invalid_type
inputType: 'textarea' (the fork)success=true, value KEPTsuccess=true, value KEPT — unchanged on purpose
CONTROL description: 'Help text'success=truesuccess=true
CONTROL bare documentsuccess=truesuccess=true

message per key, AFTER (this is the .describe() text too — one string, both channels):

RETIRED (objectui#5905) — ComponentInput.min was never read, and never published: the
manifest serializer forwards name/type/required/enum/binding/description and
this is not one of them, so an authored value was silently dropped. Delete the key; spell
the numeric domain out in description, which IS published.

max and step are the same sentence with their own key. placeholder's ends instead
with: "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 not
think 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 parses success=true and is silently stripped, which is
precisely 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 exact
pre-change spelling min: z.number().optional().describe('Minimum value') — mutating the
fact, 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 the
mirror's shape …" (describe reverts to 'Minimum value'), with max / step /
placeholder, the live-input control, the inputType fork pin and the strip contrast all
staying 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), so
vitest loads the mutated src directly and a stale dist cannot hide the mutation.

Observed — matches the prediction:

 × refuses `min`, names it in the path, and answers with its own guidance
× the four stay in the mirror's shape — a tombstone is DECLARED, just unwritable
AssertionError: min: expected true to be false // Object.is equality
AssertionError: expected 'Minimum value' to contain 'RETIRED (objectui#5905)'
Test Files 1 failed (1)
Tests 2 failed | 9 passed (11)

(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 text
count 1, remaining tombstone calls 4 (1 import + 3 keys), git hash-object
c82523ed…622c6d39….

Restore proven BOTH ways, under trap … EXIT INT TERM, absolute paths from
git rev-parse --show-toplevel, with git checkout HEAD -- ABSOLUTE_PATH (never the bare
form, which reads from the index): git diff HEAD empty and each file's
git hash-object equal to its HEAD blob, for all four touched paths. Then a
post-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:

gateverdict lineexit
pnpm --filter @object-ui/types buildtscBUILD_EXIT=0
pnpm --filter @object-ui/types type-check (hyphenated)tsc --noEmit then tsc -p tsconfig.examples.json then tsc -p tsconfig.test.jsonTYPECHECK_EXIT=0
pnpm exec vitest run packages/types/ (root form)Test Files 76 passed (76) · Tests 872 passed (872)0
pnpm exec vitest run packages/core/src/registry/Test Files 7 passed (7) · Tests 111 passed (111)0
pnpm exec vitest run packages/sdui-parser/Test Files 11 passed (11) · Tests 147 passed (147)0
pnpm exec eslint . (plain form, no --no-inline-config)✖ 11516 problems (0 errors, 11516 warnings)ESLINT_EXIT=0
node scripts/check-changeset-presence.mjs✅ 4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)0
zod-mirror-parityTest Files 1 passed (1) · Tests 5 passed (5)0

vitest file/test counts, before → after (the wrong-cwd false green this repo has been
bitten by would report apps/console's 22 files instead):

Test FilesTests
BEFORE (merge-base state of packages/types)75861
AFTER76872
delta+1+11

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 at b03ba3ad5. The
+1 file / +11 tests are exactly the new pin file.

zod-mirror-parity deserves its own note since this removes declarations it pairs: it
compares the mirror's .shape against the declaration over the INTERSECTION of the mirror's
keys 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#ComponentInputSchema has no
KnownDrift and no UnmirroredDeclared entry before or after — same precedent as
DashboardComponentSchema.aria (objectui#5855).

eslint reports 0 errors; the 11,516 warnings are the repo's pre-existing
no-explicit-any population. The four files this PR touches contribute zero new
findings
(the base.ts / base.zod.ts warnings are on untouched lines).

10. What is NOT claimed

  • Not claimed: that nothing outside this repository writes these keys. Not measurable here.
  • Not claimed: that inputType should stay. It needs a ruling; this PR does not make it.
  • Not claimed: that constraint slots on ComponentInput are wrong. See §4.
  • Not run locally: the full repo test farm and the rest of the check:* set — CI owns
    those and runs them exactly once on this branch.

os-samand others added 2 commits August 31, 2026 02:35
`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
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3179.9 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-DkRl-aik.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 03:28
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 453dbaaAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-5905-componentinput-retire-five-keys branch August 31, 2026 03:42
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@os-sam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(types)!: retire ComponentInput's four inert constraint keys, and report the inputType fork - #6949

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-5905-componentinput-retire-five-keys
Aug 31, 2026
Merged

feat(types)!: retire ComponentInput's four inert constraint keys, and report the inputType fork#6949
os-sam merged 2 commits into
mainfrom
claude/issue-5905-componentinput-retire-five-keys

Conversation

@os-sam

@os-samos-sam commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Refs #5905. ⚠️Refs, not a closing keyword — the card asked for FIVE keys and this
retires FOUR.
The fifth, inputType, hit the fork condition triage set, and the card
stays open on that fork.

needs:contract-review tier: this changes members on a published type
(@object-ui/typesComponentInput). ⛔ Draft on purpose — not ready, not enqueued, no
auto-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:

keysitewhat it writes
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) takes
meta as ComponentMeta (packages/core/src/registry/Registry.ts:352), whose inputs is
ComponentInput[]. It is also pinnedpackages/plugin-markdown/src/index.test.ts:51
asserts expect(contentInput?.inputType).toBe('textarea').

So inputType is 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. inputType is left live and
writable
, 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, and
are 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, all
top-level directories, packages/ and apps/ and examples/ and e2e/ and content/
and skills/ and scripts/ included.
Not a package subset.

Instrument (structural, not a bare grep): bracket-match every inputs: array in every
tracked .ts/.tsx/.js/.jsx/.mjs/.cjs/.json/.md/.mdx file, then collect the top-level keys
of the object literals inside — i.e. the ComponentInput's own keys. 219 regions
scanned.

The zero comes with a HOT CONTROL, from the same pass over the same regions (not
merely repo-wide):

retired candidatesitescontrol: a key the serializer DOES forwardsites
inputType1name926
min0type926
max0description161
step0enum114
placeholder0required87

The instrument was demonstrably not blind: it found the one inputType site and four
digits of control hits in the same regions.

Blind spot found and closed by hand. The inputs: scan cannot see a ComponentInput[]
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) and ELEMENT_DATA_SOURCE_INPUT
(packages/core/src/data-scope/element-data-source.ts:327).

placeholder false positives, rejected BY HAND — and that rejection is a finding about
the instrument, not noise.
A plain placeholder: grep over the same file set returns 26
hits and none of them is a ComponentInput key:

  • packages/vscode-extension/src/providers/PreviewProvider.ts:180,193 — writes
    input.placeholder / textarea.placeholder on a DOM element, from
    BaseSchema.placeholder. Different type on both ends.
  • packages/app-shell/src/views/metadata-admin/previews/block-config.ts — an inspector
    field spec whose own key is named placeholder
    (a PlaceholderSpec). Different type.
  • Eight renderer registrations (combobox, command, date-picker, select, textarea,
    and three in plugin-chatbot) write placeholder in defaultProps, the element's
    own prop — while their inputs: array carries { name: 'placeholder', … }, i.e.
    placeholder as the value of name, never as a ComponentInput key.
  • plugin-designer / ObjectManager / plugin-view / form.tsx hits are form field
    descriptors (type: 'text', type: 'input' — neither is a ComponentInputControlType).

The publication path, re-read on the merge-base rather than trusted from the card.
packages/sdui-parser/src/index.ts:153-160 forwards exactly six keys per input:

inputs: (c.inputs??[]).map((i)=>({name: i.name,type: canonicalizeInputType(i.type),required: i.required,enum: i.enum,binding: i.binding,description: i.description,})),

None of the five is in it, so a value authored here could not reach the published
sdui.manifest.json even in principle.

Reads: none. A member-access census across the 71 files that consume .inputs returns
nothing for min / max / step / placeholder on a ComponentInput value (the hits are
i18n key strings, sim.step(), and element:text_input.placeholder, which is a different
surface). Same pass, same file set, control: .name 188, .type 302, .description 68,
.enum 28, .required 23.

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:
ComponentInputSchema is a non-strict z.object, so a deleted key would be silently
stripped
— 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 type field carries a maintainer ruling of 2026-08-17, quoted in
base.ts:

Ruled (maintainer, 2026-08-17): the coarse arm plus description IS the publication
face's expression ceiling today, and SPEC IS THE SOLE JUDGE OF VALUES.

Two directions were deferred, not rejected on merit: giving ComponentInput real
constraint slots (two sources of truth, free to drift), and binding checkType to spec's
Zod member when a ComponentPropsMap entry exists (one truth, but couples sdui-parser
to spec). The ruling names the reopen condition: a measured case of an author — human
or agent — shipping a spec-rejected value that objectui's silence let through.

min / max / step read exactly like the slots that ruling declined to add. What is
retired 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

Considered and deliberately not touched: packages/core/src/registry/Registry.ts's
doc 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/docs page and no published skills file teaches
these four on ComponentInput — the skill's own ComponentInput type block
(skills/objectui/guides/plugin-development.md:87) already lists eight keys and none of the
five. This diff does not touch the published skills directory at all.

6. ⚠️ The emitted .d.ts, measured on BOTH sides — what the contract reviewer needs

Built with pnpm --filter @object-ui/types build on each side, then diffed.

packages/types/dist/base.d.tsinterface ComponentInput (comments stripped):

 description?: string;
advanced?: boolean;
inputType?: string;
- min?: number;- max?: number;- step?: number;- placeholder?: string;+ min?: never;+ max?: never;+ step?: never;+ placeholder?: never;
}

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 / string to never. That is the tombstone contract: the key stays visible so an
author 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.
⚠️ The angle-bracket generic spelling is written out in words below on purpose: this
body's sanitizer eats tag-shaped fragments, and on the first publish it ate exactly this
table — both columns came back reading z.ZodOptional and the diff was therefore
meaningless. Read "OPT of X" as z.ZodOptional parameterised by X.

memberBEFOREAFTER
minOPT of z.ZodNumberOPT of z.ZodNever
maxOPT of z.ZodNumberOPT of z.ZodNever
stepOPT of z.ZodNumberOPT of z.ZodNever
placeholderOPT of z.ZodStringOPT of z.ZodNever

That shape is inlined three times in that one file — ComponentInputSchema,
ComponentMetaSchema and ComponentConfigSchema all carry it.

Which published subpaths carry the change (from package.jsonexports):

subpathfilehow it carries it
@object-ui/types/basedist/base.d.tsthe declaration itself
@object-ui/typesdist/index.d.tsre-exports the type by name (ComponentInput)
@object-ui/typesdist/index.d.tsre-exports it again as the deprecated alias PluginComponentInput (via dist/plugin-scope.d.ts)
@object-ui/types/zoddist/zod/index.zod.d.tsdist/zod/base.zod.d.tsComponentInputSchema, ComponentMetaSchema, ComponentConfigSchema

dist/widget.d.ts mentions ComponentInput only in prose and imports
ComponentInputControlType; WidgetInput's own eight-key shape is unchanged.

7. The refusal is real — before/after safeParse, with a control both runs

Same probe, same document shape, run against the mirror before and after the change.
Control document: { name: 'content', type: 'string', label: …, required: true }.

key writtenBEFOREAFTER
min: 0success=true, value KEPTsuccess=false, path ["min"], code invalid_type
max: 100success=true, value KEPTsuccess=false, path ["max"], code invalid_type
step: 1success=true, value KEPTsuccess=false, path ["step"], code invalid_type
placeholder: 'Type here…'success=true, value KEPTsuccess=false, path ["placeholder"], code invalid_type
inputType: 'textarea' (the fork)success=true, value KEPTsuccess=true, value KEPT — unchanged on purpose
CONTROL description: 'Help text'success=truesuccess=true
CONTROL bare documentsuccess=truesuccess=true

message per key, AFTER (this is the .describe() text too — one string, both channels):

RETIRED (objectui#5905) — ComponentInput.min was never read, and never published: the
manifest serializer forwards name/type/required/enum/binding/description and
this is not one of them, so an authored value was silently dropped. Delete the key; spell
the numeric domain out in description, which IS published.

max and step are the same sentence with their own key. placeholder's ends instead
with: "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 not
think 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 parses success=true and is silently stripped, which is
precisely 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 exact
pre-change spelling min: z.number().optional().describe('Minimum value') — mutating the
fact, 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 the
mirror's shape …" (describe reverts to 'Minimum value'), with max / step /
placeholder, the live-input control, the inputType fork pin and the strip contrast all
staying 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), so
vitest loads the mutated src directly and a stale dist cannot hide the mutation.

Observed — matches the prediction:

 × refuses `min`, names it in the path, and answers with its own guidance
× the four stay in the mirror's shape — a tombstone is DECLARED, just unwritable
AssertionError: min: expected true to be false // Object.is equality
AssertionError: expected 'Minimum value' to contain 'RETIRED (objectui#5905)'
Test Files 1 failed (1)
Tests 2 failed | 9 passed (11)

(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 text
count 1, remaining tombstone calls 4 (1 import + 3 keys), git hash-object
c82523ed…622c6d39….

Restore proven BOTH ways, under trap … EXIT INT TERM, absolute paths from
git rev-parse --show-toplevel, with git checkout HEAD -- ABSOLUTE_PATH (never the bare
form, which reads from the index): git diff HEAD empty and each file's
git hash-object equal to its HEAD blob, for all four touched paths. Then a
post-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:

gateverdict lineexit
pnpm --filter @object-ui/types buildtscBUILD_EXIT=0
pnpm --filter @object-ui/types type-check (hyphenated)tsc --noEmit then tsc -p tsconfig.examples.json then tsc -p tsconfig.test.jsonTYPECHECK_EXIT=0
pnpm exec vitest run packages/types/ (root form)Test Files 76 passed (76) · Tests 872 passed (872)0
pnpm exec vitest run packages/core/src/registry/Test Files 7 passed (7) · Tests 111 passed (111)0
pnpm exec vitest run packages/sdui-parser/Test Files 11 passed (11) · Tests 147 passed (147)0
pnpm exec eslint . (plain form, no --no-inline-config)✖ 11516 problems (0 errors, 11516 warnings)ESLINT_EXIT=0
node scripts/check-changeset-presence.mjs✅ 4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)0
zod-mirror-parityTest Files 1 passed (1) · Tests 5 passed (5)0

vitest file/test counts, before → after (the wrong-cwd false green this repo has been
bitten by would report apps/console's 22 files instead):

Test FilesTests
BEFORE (merge-base state of packages/types)75861
AFTER76872
delta+1+11

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 at b03ba3ad5. The
+1 file / +11 tests are exactly the new pin file.

zod-mirror-parity deserves its own note since this removes declarations it pairs: it
compares the mirror's .shape against the declaration over the INTERSECTION of the mirror's
keys 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#ComponentInputSchema has no
KnownDrift and no UnmirroredDeclared entry before or after — same precedent as
DashboardComponentSchema.aria (objectui#5855).

eslint reports 0 errors; the 11,516 warnings are the repo's pre-existing
no-explicit-any population. The four files this PR touches contribute zero new
findings
(the base.ts / base.zod.ts warnings are on untouched lines).

10. What is NOT claimed

  • Not claimed: that nothing outside this repository writes these keys. Not measurable here.
  • Not claimed: that inputType should stay. It needs a ruling; this PR does not make it.
  • Not claimed: that constraint slots on ComponentInput are wrong. See §4.
  • Not run locally: the full repo test farm and the rest of the check:* set — CI owns
    those and runs them exactly once on this branch.

os-samand others added 2 commits August 31, 2026 02:35
`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
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3179.9 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-DkRl-aik.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 03:28
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 453dbaaAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-5905-componentinput-retire-five-keys branch August 31, 2026 03:42
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@os-sam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(types)!: retire ComponentInput's four inert constraint keys, and report the inputType fork - #6949

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-5905-componentinput-retire-five-keys
Aug 31, 2026
Merged

feat(types)!: retire ComponentInput's four inert constraint keys, and report the inputType fork#6949
os-sam merged 2 commits into
mainfrom
claude/issue-5905-componentinput-retire-five-keys

Conversation

@os-sam

@os-samos-sam commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Refs #5905. ⚠️Refs, not a closing keyword — the card asked for FIVE keys and this
retires FOUR.
The fifth, inputType, hit the fork condition triage set, and the card
stays open on that fork.

needs:contract-review tier: this changes members on a published type
(@object-ui/typesComponentInput). ⛔ Draft on purpose — not ready, not enqueued, no
auto-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:

keysitewhat it writes
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) takes
meta as ComponentMeta (packages/core/src/registry/Registry.ts:352), whose inputs is
ComponentInput[]. It is also pinnedpackages/plugin-markdown/src/index.test.ts:51
asserts expect(contentInput?.inputType).toBe('textarea').

So inputType is 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. inputType is left live and
writable
, 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, and
are 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, all
top-level directories, packages/ and apps/ and examples/ and e2e/ and content/
and skills/ and scripts/ included.
Not a package subset.

Instrument (structural, not a bare grep): bracket-match every inputs: array in every
tracked .ts/.tsx/.js/.jsx/.mjs/.cjs/.json/.md/.mdx file, then collect the top-level keys
of the object literals inside — i.e. the ComponentInput's own keys. 219 regions
scanned.

The zero comes with a HOT CONTROL, from the same pass over the same regions (not
merely repo-wide):

retired candidatesitescontrol: a key the serializer DOES forwardsites
inputType1name926
min0type926
max0description161
step0enum114
placeholder0required87

The instrument was demonstrably not blind: it found the one inputType site and four
digits of control hits in the same regions.

Blind spot found and closed by hand. The inputs: scan cannot see a ComponentInput[]
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) and ELEMENT_DATA_SOURCE_INPUT
(packages/core/src/data-scope/element-data-source.ts:327).

placeholder false positives, rejected BY HAND — and that rejection is a finding about
the instrument, not noise.
A plain placeholder: grep over the same file set returns 26
hits and none of them is a ComponentInput key:

  • packages/vscode-extension/src/providers/PreviewProvider.ts:180,193 — writes
    input.placeholder / textarea.placeholder on a DOM element, from
    BaseSchema.placeholder. Different type on both ends.
  • packages/app-shell/src/views/metadata-admin/previews/block-config.ts — an inspector
    field spec whose own key is named placeholder
    (a PlaceholderSpec). Different type.
  • Eight renderer registrations (combobox, command, date-picker, select, textarea,
    and three in plugin-chatbot) write placeholder in defaultProps, the element's
    own prop — while their inputs: array carries { name: 'placeholder', … }, i.e.
    placeholder as the value of name, never as a ComponentInput key.
  • plugin-designer / ObjectManager / plugin-view / form.tsx hits are form field
    descriptors (type: 'text', type: 'input' — neither is a ComponentInputControlType).

The publication path, re-read on the merge-base rather than trusted from the card.
packages/sdui-parser/src/index.ts:153-160 forwards exactly six keys per input:

inputs: (c.inputs??[]).map((i)=>({name: i.name,type: canonicalizeInputType(i.type),required: i.required,enum: i.enum,binding: i.binding,description: i.description,})),

None of the five is in it, so a value authored here could not reach the published
sdui.manifest.json even in principle.

Reads: none. A member-access census across the 71 files that consume .inputs returns
nothing for min / max / step / placeholder on a ComponentInput value (the hits are
i18n key strings, sim.step(), and element:text_input.placeholder, which is a different
surface). Same pass, same file set, control: .name 188, .type 302, .description 68,
.enum 28, .required 23.

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:
ComponentInputSchema is a non-strict z.object, so a deleted key would be silently
stripped
— 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 type field carries a maintainer ruling of 2026-08-17, quoted in
base.ts:

Ruled (maintainer, 2026-08-17): the coarse arm plus description IS the publication
face's expression ceiling today, and SPEC IS THE SOLE JUDGE OF VALUES.

Two directions were deferred, not rejected on merit: giving ComponentInput real
constraint slots (two sources of truth, free to drift), and binding checkType to spec's
Zod member when a ComponentPropsMap entry exists (one truth, but couples sdui-parser
to spec). The ruling names the reopen condition: a measured case of an author — human
or agent — shipping a spec-rejected value that objectui's silence let through.

min / max / step read exactly like the slots that ruling declined to add. What is
retired 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

Considered and deliberately not touched: packages/core/src/registry/Registry.ts's
doc 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/docs page and no published skills file teaches
these four on ComponentInput — the skill's own ComponentInput type block
(skills/objectui/guides/plugin-development.md:87) already lists eight keys and none of the
five. This diff does not touch the published skills directory at all.

6. ⚠️ The emitted .d.ts, measured on BOTH sides — what the contract reviewer needs

Built with pnpm --filter @object-ui/types build on each side, then diffed.

packages/types/dist/base.d.tsinterface ComponentInput (comments stripped):

 description?: string;
advanced?: boolean;
inputType?: string;
- min?: number;- max?: number;- step?: number;- placeholder?: string;+ min?: never;+ max?: never;+ step?: never;+ placeholder?: never;
}

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 / string to never. That is the tombstone contract: the key stays visible so an
author 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.
⚠️ The angle-bracket generic spelling is written out in words below on purpose: this
body's sanitizer eats tag-shaped fragments, and on the first publish it ate exactly this
table — both columns came back reading z.ZodOptional and the diff was therefore
meaningless. Read "OPT of X" as z.ZodOptional parameterised by X.

memberBEFOREAFTER
minOPT of z.ZodNumberOPT of z.ZodNever
maxOPT of z.ZodNumberOPT of z.ZodNever
stepOPT of z.ZodNumberOPT of z.ZodNever
placeholderOPT of z.ZodStringOPT of z.ZodNever

That shape is inlined three times in that one file — ComponentInputSchema,
ComponentMetaSchema and ComponentConfigSchema all carry it.

Which published subpaths carry the change (from package.jsonexports):

subpathfilehow it carries it
@object-ui/types/basedist/base.d.tsthe declaration itself
@object-ui/typesdist/index.d.tsre-exports the type by name (ComponentInput)
@object-ui/typesdist/index.d.tsre-exports it again as the deprecated alias PluginComponentInput (via dist/plugin-scope.d.ts)
@object-ui/types/zoddist/zod/index.zod.d.tsdist/zod/base.zod.d.tsComponentInputSchema, ComponentMetaSchema, ComponentConfigSchema

dist/widget.d.ts mentions ComponentInput only in prose and imports
ComponentInputControlType; WidgetInput's own eight-key shape is unchanged.

7. The refusal is real — before/after safeParse, with a control both runs

Same probe, same document shape, run against the mirror before and after the change.
Control document: { name: 'content', type: 'string', label: …, required: true }.

key writtenBEFOREAFTER
min: 0success=true, value KEPTsuccess=false, path ["min"], code invalid_type
max: 100success=true, value KEPTsuccess=false, path ["max"], code invalid_type
step: 1success=true, value KEPTsuccess=false, path ["step"], code invalid_type
placeholder: 'Type here…'success=true, value KEPTsuccess=false, path ["placeholder"], code invalid_type
inputType: 'textarea' (the fork)success=true, value KEPTsuccess=true, value KEPT — unchanged on purpose
CONTROL description: 'Help text'success=truesuccess=true
CONTROL bare documentsuccess=truesuccess=true

message per key, AFTER (this is the .describe() text too — one string, both channels):

RETIRED (objectui#5905) — ComponentInput.min was never read, and never published: the
manifest serializer forwards name/type/required/enum/binding/description and
this is not one of them, so an authored value was silently dropped. Delete the key; spell
the numeric domain out in description, which IS published.

max and step are the same sentence with their own key. placeholder's ends instead
with: "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 not
think 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 parses success=true and is silently stripped, which is
precisely 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 exact
pre-change spelling min: z.number().optional().describe('Minimum value') — mutating the
fact, 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 the
mirror's shape …" (describe reverts to 'Minimum value'), with max / step /
placeholder, the live-input control, the inputType fork pin and the strip contrast all
staying 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), so
vitest loads the mutated src directly and a stale dist cannot hide the mutation.

Observed — matches the prediction:

 × refuses `min`, names it in the path, and answers with its own guidance
× the four stay in the mirror's shape — a tombstone is DECLARED, just unwritable
AssertionError: min: expected true to be false // Object.is equality
AssertionError: expected 'Minimum value' to contain 'RETIRED (objectui#5905)'
Test Files 1 failed (1)
Tests 2 failed | 9 passed (11)

(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 text
count 1, remaining tombstone calls 4 (1 import + 3 keys), git hash-object
c82523ed…622c6d39….

Restore proven BOTH ways, under trap … EXIT INT TERM, absolute paths from
git rev-parse --show-toplevel, with git checkout HEAD -- ABSOLUTE_PATH (never the bare
form, which reads from the index): git diff HEAD empty and each file's
git hash-object equal to its HEAD blob, for all four touched paths. Then a
post-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:

gateverdict lineexit
pnpm --filter @object-ui/types buildtscBUILD_EXIT=0
pnpm --filter @object-ui/types type-check (hyphenated)tsc --noEmit then tsc -p tsconfig.examples.json then tsc -p tsconfig.test.jsonTYPECHECK_EXIT=0
pnpm exec vitest run packages/types/ (root form)Test Files 76 passed (76) · Tests 872 passed (872)0
pnpm exec vitest run packages/core/src/registry/Test Files 7 passed (7) · Tests 111 passed (111)0
pnpm exec vitest run packages/sdui-parser/Test Files 11 passed (11) · Tests 147 passed (147)0
pnpm exec eslint . (plain form, no --no-inline-config)✖ 11516 problems (0 errors, 11516 warnings)ESLINT_EXIT=0
node scripts/check-changeset-presence.mjs✅ 4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)0
zod-mirror-parityTest Files 1 passed (1) · Tests 5 passed (5)0

vitest file/test counts, before → after (the wrong-cwd false green this repo has been
bitten by would report apps/console's 22 files instead):

Test FilesTests
BEFORE (merge-base state of packages/types)75861
AFTER76872
delta+1+11

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 at b03ba3ad5. The
+1 file / +11 tests are exactly the new pin file.

zod-mirror-parity deserves its own note since this removes declarations it pairs: it
compares the mirror's .shape against the declaration over the INTERSECTION of the mirror's
keys 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#ComponentInputSchema has no
KnownDrift and no UnmirroredDeclared entry before or after — same precedent as
DashboardComponentSchema.aria (objectui#5855).

eslint reports 0 errors; the 11,516 warnings are the repo's pre-existing
no-explicit-any population. The four files this PR touches contribute zero new
findings
(the base.ts / base.zod.ts warnings are on untouched lines).

10. What is NOT claimed

  • Not claimed: that nothing outside this repository writes these keys. Not measurable here.
  • Not claimed: that inputType should stay. It needs a ruling; this PR does not make it.
  • Not claimed: that constraint slots on ComponentInput are wrong. See §4.
  • Not run locally: the full repo test farm and the rest of the check:* set — CI owns
    those and runs them exactly once on this branch.

os-samand others added 2 commits August 31, 2026 02:35
`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
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3179.9 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-DkRl-aik.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 03:28
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 453dbaaAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-5905-componentinput-retire-five-keys branch August 31, 2026 03:42
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@os-sam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(types)!: retire ComponentInput's four inert constraint keys, and report the inputType fork - #6949

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-5905-componentinput-retire-five-keys
Aug 31, 2026
Merged

feat(types)!: retire ComponentInput's four inert constraint keys, and report the inputType fork#6949
os-sam merged 2 commits into
mainfrom
claude/issue-5905-componentinput-retire-five-keys

Conversation

@os-sam

@os-samos-sam commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Refs #5905. ⚠️Refs, not a closing keyword — the card asked for FIVE keys and this
retires FOUR.
The fifth, inputType, hit the fork condition triage set, and the card
stays open on that fork.

needs:contract-review tier: this changes members on a published type
(@object-ui/typesComponentInput). ⛔ Draft on purpose — not ready, not enqueued, no
auto-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:

keysitewhat it writes
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) takes
meta as ComponentMeta (packages/core/src/registry/Registry.ts:352), whose inputs is
ComponentInput[]. It is also pinnedpackages/plugin-markdown/src/index.test.ts:51
asserts expect(contentInput?.inputType).toBe('textarea').

So inputType is 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. inputType is left live and
writable
, 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, and
are 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, all
top-level directories, packages/ and apps/ and examples/ and e2e/ and content/
and skills/ and scripts/ included.
Not a package subset.

Instrument (structural, not a bare grep): bracket-match every inputs: array in every
tracked .ts/.tsx/.js/.jsx/.mjs/.cjs/.json/.md/.mdx file, then collect the top-level keys
of the object literals inside — i.e. the ComponentInput's own keys. 219 regions
scanned.

The zero comes with a HOT CONTROL, from the same pass over the same regions (not
merely repo-wide):

retired candidatesitescontrol: a key the serializer DOES forwardsites
inputType1name926
min0type926
max0description161
step0enum114
placeholder0required87

The instrument was demonstrably not blind: it found the one inputType site and four
digits of control hits in the same regions.

Blind spot found and closed by hand. The inputs: scan cannot see a ComponentInput[]
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) and ELEMENT_DATA_SOURCE_INPUT
(packages/core/src/data-scope/element-data-source.ts:327).

placeholder false positives, rejected BY HAND — and that rejection is a finding about
the instrument, not noise.
A plain placeholder: grep over the same file set returns 26
hits and none of them is a ComponentInput key:

  • packages/vscode-extension/src/providers/PreviewProvider.ts:180,193 — writes
    input.placeholder / textarea.placeholder on a DOM element, from
    BaseSchema.placeholder. Different type on both ends.
  • packages/app-shell/src/views/metadata-admin/previews/block-config.ts — an inspector
    field spec whose own key is named placeholder
    (a PlaceholderSpec). Different type.
  • Eight renderer registrations (combobox, command, date-picker, select, textarea,
    and three in plugin-chatbot) write placeholder in defaultProps, the element's
    own prop — while their inputs: array carries { name: 'placeholder', … }, i.e.
    placeholder as the value of name, never as a ComponentInput key.
  • plugin-designer / ObjectManager / plugin-view / form.tsx hits are form field
    descriptors (type: 'text', type: 'input' — neither is a ComponentInputControlType).

The publication path, re-read on the merge-base rather than trusted from the card.
packages/sdui-parser/src/index.ts:153-160 forwards exactly six keys per input:

inputs: (c.inputs??[]).map((i)=>({name: i.name,type: canonicalizeInputType(i.type),required: i.required,enum: i.enum,binding: i.binding,description: i.description,})),

None of the five is in it, so a value authored here could not reach the published
sdui.manifest.json even in principle.

Reads: none. A member-access census across the 71 files that consume .inputs returns
nothing for min / max / step / placeholder on a ComponentInput value (the hits are
i18n key strings, sim.step(), and element:text_input.placeholder, which is a different
surface). Same pass, same file set, control: .name 188, .type 302, .description 68,
.enum 28, .required 23.

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:
ComponentInputSchema is a non-strict z.object, so a deleted key would be silently
stripped
— 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 type field carries a maintainer ruling of 2026-08-17, quoted in
base.ts:

Ruled (maintainer, 2026-08-17): the coarse arm plus description IS the publication
face's expression ceiling today, and SPEC IS THE SOLE JUDGE OF VALUES.

Two directions were deferred, not rejected on merit: giving ComponentInput real
constraint slots (two sources of truth, free to drift), and binding checkType to spec's
Zod member when a ComponentPropsMap entry exists (one truth, but couples sdui-parser
to spec). The ruling names the reopen condition: a measured case of an author — human
or agent — shipping a spec-rejected value that objectui's silence let through.

min / max / step read exactly like the slots that ruling declined to add. What is
retired 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

Considered and deliberately not touched: packages/core/src/registry/Registry.ts's
doc 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/docs page and no published skills file teaches
these four on ComponentInput — the skill's own ComponentInput type block
(skills/objectui/guides/plugin-development.md:87) already lists eight keys and none of the
five. This diff does not touch the published skills directory at all.

6. ⚠️ The emitted .d.ts, measured on BOTH sides — what the contract reviewer needs

Built with pnpm --filter @object-ui/types build on each side, then diffed.

packages/types/dist/base.d.tsinterface ComponentInput (comments stripped):

 description?: string;
advanced?: boolean;
inputType?: string;
- min?: number;- max?: number;- step?: number;- placeholder?: string;+ min?: never;+ max?: never;+ step?: never;+ placeholder?: never;
}

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 / string to never. That is the tombstone contract: the key stays visible so an
author 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.
⚠️ The angle-bracket generic spelling is written out in words below on purpose: this
body's sanitizer eats tag-shaped fragments, and on the first publish it ate exactly this
table — both columns came back reading z.ZodOptional and the diff was therefore
meaningless. Read "OPT of X" as z.ZodOptional parameterised by X.

memberBEFOREAFTER
minOPT of z.ZodNumberOPT of z.ZodNever
maxOPT of z.ZodNumberOPT of z.ZodNever
stepOPT of z.ZodNumberOPT of z.ZodNever
placeholderOPT of z.ZodStringOPT of z.ZodNever

That shape is inlined three times in that one file — ComponentInputSchema,
ComponentMetaSchema and ComponentConfigSchema all carry it.

Which published subpaths carry the change (from package.jsonexports):

subpathfilehow it carries it
@object-ui/types/basedist/base.d.tsthe declaration itself
@object-ui/typesdist/index.d.tsre-exports the type by name (ComponentInput)
@object-ui/typesdist/index.d.tsre-exports it again as the deprecated alias PluginComponentInput (via dist/plugin-scope.d.ts)
@object-ui/types/zoddist/zod/index.zod.d.tsdist/zod/base.zod.d.tsComponentInputSchema, ComponentMetaSchema, ComponentConfigSchema

dist/widget.d.ts mentions ComponentInput only in prose and imports
ComponentInputControlType; WidgetInput's own eight-key shape is unchanged.

7. The refusal is real — before/after safeParse, with a control both runs

Same probe, same document shape, run against the mirror before and after the change.
Control document: { name: 'content', type: 'string', label: …, required: true }.

key writtenBEFOREAFTER
min: 0success=true, value KEPTsuccess=false, path ["min"], code invalid_type
max: 100success=true, value KEPTsuccess=false, path ["max"], code invalid_type
step: 1success=true, value KEPTsuccess=false, path ["step"], code invalid_type
placeholder: 'Type here…'success=true, value KEPTsuccess=false, path ["placeholder"], code invalid_type
inputType: 'textarea' (the fork)success=true, value KEPTsuccess=true, value KEPT — unchanged on purpose
CONTROL description: 'Help text'success=truesuccess=true
CONTROL bare documentsuccess=truesuccess=true

message per key, AFTER (this is the .describe() text too — one string, both channels):

RETIRED (objectui#5905) — ComponentInput.min was never read, and never published: the
manifest serializer forwards name/type/required/enum/binding/description and
this is not one of them, so an authored value was silently dropped. Delete the key; spell
the numeric domain out in description, which IS published.

max and step are the same sentence with their own key. placeholder's ends instead
with: "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 not
think 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 parses success=true and is silently stripped, which is
precisely 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 exact
pre-change spelling min: z.number().optional().describe('Minimum value') — mutating the
fact, 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 the
mirror's shape …" (describe reverts to 'Minimum value'), with max / step /
placeholder, the live-input control, the inputType fork pin and the strip contrast all
staying 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), so
vitest loads the mutated src directly and a stale dist cannot hide the mutation.

Observed — matches the prediction:

 × refuses `min`, names it in the path, and answers with its own guidance
× the four stay in the mirror's shape — a tombstone is DECLARED, just unwritable
AssertionError: min: expected true to be false // Object.is equality
AssertionError: expected 'Minimum value' to contain 'RETIRED (objectui#5905)'
Test Files 1 failed (1)
Tests 2 failed | 9 passed (11)

(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 text
count 1, remaining tombstone calls 4 (1 import + 3 keys), git hash-object
c82523ed…622c6d39….

Restore proven BOTH ways, under trap … EXIT INT TERM, absolute paths from
git rev-parse --show-toplevel, with git checkout HEAD -- ABSOLUTE_PATH (never the bare
form, which reads from the index): git diff HEAD empty and each file's
git hash-object equal to its HEAD blob, for all four touched paths. Then a
post-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:

gateverdict lineexit
pnpm --filter @object-ui/types buildtscBUILD_EXIT=0
pnpm --filter @object-ui/types type-check (hyphenated)tsc --noEmit then tsc -p tsconfig.examples.json then tsc -p tsconfig.test.jsonTYPECHECK_EXIT=0
pnpm exec vitest run packages/types/ (root form)Test Files 76 passed (76) · Tests 872 passed (872)0
pnpm exec vitest run packages/core/src/registry/Test Files 7 passed (7) · Tests 111 passed (111)0
pnpm exec vitest run packages/sdui-parser/Test Files 11 passed (11) · Tests 147 passed (147)0
pnpm exec eslint . (plain form, no --no-inline-config)✖ 11516 problems (0 errors, 11516 warnings)ESLINT_EXIT=0
node scripts/check-changeset-presence.mjs✅ 4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)0
zod-mirror-parityTest Files 1 passed (1) · Tests 5 passed (5)0

vitest file/test counts, before → after (the wrong-cwd false green this repo has been
bitten by would report apps/console's 22 files instead):

Test FilesTests
BEFORE (merge-base state of packages/types)75861
AFTER76872
delta+1+11

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 at b03ba3ad5. The
+1 file / +11 tests are exactly the new pin file.

zod-mirror-parity deserves its own note since this removes declarations it pairs: it
compares the mirror's .shape against the declaration over the INTERSECTION of the mirror's
keys 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#ComponentInputSchema has no
KnownDrift and no UnmirroredDeclared entry before or after — same precedent as
DashboardComponentSchema.aria (objectui#5855).

eslint reports 0 errors; the 11,516 warnings are the repo's pre-existing
no-explicit-any population. The four files this PR touches contribute zero new
findings
(the base.ts / base.zod.ts warnings are on untouched lines).

10. What is NOT claimed

  • Not claimed: that nothing outside this repository writes these keys. Not measurable here.
  • Not claimed: that inputType should stay. It needs a ruling; this PR does not make it.
  • Not claimed: that constraint slots on ComponentInput are wrong. See §4.
  • Not run locally: the full repo test farm and the rest of the check:* set — CI owns
    those and runs them exactly once on this branch.

os-samand others added 2 commits August 31, 2026 02:35
`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
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3179.9 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-DkRl-aik.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 03:28
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 453dbaaAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-5905-componentinput-retire-five-keys branch August 31, 2026 03:42
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@os-sam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(types)!: retire ComponentInput's four inert constraint keys, and report the inputType fork - #6949

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-5905-componentinput-retire-five-keys
Aug 31, 2026
Merged

feat(types)!: retire ComponentInput's four inert constraint keys, and report the inputType fork#6949
os-sam merged 2 commits into
mainfrom
claude/issue-5905-componentinput-retire-five-keys

Conversation

@os-sam

@os-samos-sam commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Refs #5905. ⚠️Refs, not a closing keyword — the card asked for FIVE keys and this
retires FOUR.
The fifth, inputType, hit the fork condition triage set, and the card
stays open on that fork.

needs:contract-review tier: this changes members on a published type
(@object-ui/typesComponentInput). ⛔ Draft on purpose — not ready, not enqueued, no
auto-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:

keysitewhat it writes
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) takes
meta as ComponentMeta (packages/core/src/registry/Registry.ts:352), whose inputs is
ComponentInput[]. It is also pinnedpackages/plugin-markdown/src/index.test.ts:51
asserts expect(contentInput?.inputType).toBe('textarea').

So inputType is 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. inputType is left live and
writable
, 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, and
are 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, all
top-level directories, packages/ and apps/ and examples/ and e2e/ and content/
and skills/ and scripts/ included.
Not a package subset.

Instrument (structural, not a bare grep): bracket-match every inputs: array in every
tracked .ts/.tsx/.js/.jsx/.mjs/.cjs/.json/.md/.mdx file, then collect the top-level keys
of the object literals inside — i.e. the ComponentInput's own keys. 219 regions
scanned.

The zero comes with a HOT CONTROL, from the same pass over the same regions (not
merely repo-wide):

retired candidatesitescontrol: a key the serializer DOES forwardsites
inputType1name926
min0type926
max0description161
step0enum114
placeholder0required87

The instrument was demonstrably not blind: it found the one inputType site and four
digits of control hits in the same regions.

Blind spot found and closed by hand. The inputs: scan cannot see a ComponentInput[]
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) and ELEMENT_DATA_SOURCE_INPUT
(packages/core/src/data-scope/element-data-source.ts:327).

placeholder false positives, rejected BY HAND — and that rejection is a finding about
the instrument, not noise.
A plain placeholder: grep over the same file set returns 26
hits and none of them is a ComponentInput key:

  • packages/vscode-extension/src/providers/PreviewProvider.ts:180,193 — writes
    input.placeholder / textarea.placeholder on a DOM element, from
    BaseSchema.placeholder. Different type on both ends.
  • packages/app-shell/src/views/metadata-admin/previews/block-config.ts — an inspector
    field spec whose own key is named placeholder
    (a PlaceholderSpec). Different type.
  • Eight renderer registrations (combobox, command, date-picker, select, textarea,
    and three in plugin-chatbot) write placeholder in defaultProps, the element's
    own prop — while their inputs: array carries { name: 'placeholder', … }, i.e.
    placeholder as the value of name, never as a ComponentInput key.
  • plugin-designer / ObjectManager / plugin-view / form.tsx hits are form field
    descriptors (type: 'text', type: 'input' — neither is a ComponentInputControlType).

The publication path, re-read on the merge-base rather than trusted from the card.
packages/sdui-parser/src/index.ts:153-160 forwards exactly six keys per input:

inputs: (c.inputs??[]).map((i)=>({name: i.name,type: canonicalizeInputType(i.type),required: i.required,enum: i.enum,binding: i.binding,description: i.description,})),

None of the five is in it, so a value authored here could not reach the published
sdui.manifest.json even in principle.

Reads: none. A member-access census across the 71 files that consume .inputs returns
nothing for min / max / step / placeholder on a ComponentInput value (the hits are
i18n key strings, sim.step(), and element:text_input.placeholder, which is a different
surface). Same pass, same file set, control: .name 188, .type 302, .description 68,
.enum 28, .required 23.

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:
ComponentInputSchema is a non-strict z.object, so a deleted key would be silently
stripped
— 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 type field carries a maintainer ruling of 2026-08-17, quoted in
base.ts:

Ruled (maintainer, 2026-08-17): the coarse arm plus description IS the publication
face's expression ceiling today, and SPEC IS THE SOLE JUDGE OF VALUES.

Two directions were deferred, not rejected on merit: giving ComponentInput real
constraint slots (two sources of truth, free to drift), and binding checkType to spec's
Zod member when a ComponentPropsMap entry exists (one truth, but couples sdui-parser
to spec). The ruling names the reopen condition: a measured case of an author — human
or agent — shipping a spec-rejected value that objectui's silence let through.

min / max / step read exactly like the slots that ruling declined to add. What is
retired 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

Considered and deliberately not touched: packages/core/src/registry/Registry.ts's
doc 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/docs page and no published skills file teaches
these four on ComponentInput — the skill's own ComponentInput type block
(skills/objectui/guides/plugin-development.md:87) already lists eight keys and none of the
five. This diff does not touch the published skills directory at all.

6. ⚠️ The emitted .d.ts, measured on BOTH sides — what the contract reviewer needs

Built with pnpm --filter @object-ui/types build on each side, then diffed.

packages/types/dist/base.d.tsinterface ComponentInput (comments stripped):

 description?: string;
advanced?: boolean;
inputType?: string;
- min?: number;- max?: number;- step?: number;- placeholder?: string;+ min?: never;+ max?: never;+ step?: never;+ placeholder?: never;
}

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 / string to never. That is the tombstone contract: the key stays visible so an
author 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.
⚠️ The angle-bracket generic spelling is written out in words below on purpose: this
body's sanitizer eats tag-shaped fragments, and on the first publish it ate exactly this
table — both columns came back reading z.ZodOptional and the diff was therefore
meaningless. Read "OPT of X" as z.ZodOptional parameterised by X.

memberBEFOREAFTER
minOPT of z.ZodNumberOPT of z.ZodNever
maxOPT of z.ZodNumberOPT of z.ZodNever
stepOPT of z.ZodNumberOPT of z.ZodNever
placeholderOPT of z.ZodStringOPT of z.ZodNever

That shape is inlined three times in that one file — ComponentInputSchema,
ComponentMetaSchema and ComponentConfigSchema all carry it.

Which published subpaths carry the change (from package.jsonexports):

subpathfilehow it carries it
@object-ui/types/basedist/base.d.tsthe declaration itself
@object-ui/typesdist/index.d.tsre-exports the type by name (ComponentInput)
@object-ui/typesdist/index.d.tsre-exports it again as the deprecated alias PluginComponentInput (via dist/plugin-scope.d.ts)
@object-ui/types/zoddist/zod/index.zod.d.tsdist/zod/base.zod.d.tsComponentInputSchema, ComponentMetaSchema, ComponentConfigSchema

dist/widget.d.ts mentions ComponentInput only in prose and imports
ComponentInputControlType; WidgetInput's own eight-key shape is unchanged.

7. The refusal is real — before/after safeParse, with a control both runs

Same probe, same document shape, run against the mirror before and after the change.
Control document: { name: 'content', type: 'string', label: …, required: true }.

key writtenBEFOREAFTER
min: 0success=true, value KEPTsuccess=false, path ["min"], code invalid_type
max: 100success=true, value KEPTsuccess=false, path ["max"], code invalid_type
step: 1success=true, value KEPTsuccess=false, path ["step"], code invalid_type
placeholder: 'Type here…'success=true, value KEPTsuccess=false, path ["placeholder"], code invalid_type
inputType: 'textarea' (the fork)success=true, value KEPTsuccess=true, value KEPT — unchanged on purpose
CONTROL description: 'Help text'success=truesuccess=true
CONTROL bare documentsuccess=truesuccess=true

message per key, AFTER (this is the .describe() text too — one string, both channels):

RETIRED (objectui#5905) — ComponentInput.min was never read, and never published: the
manifest serializer forwards name/type/required/enum/binding/description and
this is not one of them, so an authored value was silently dropped. Delete the key; spell
the numeric domain out in description, which IS published.

max and step are the same sentence with their own key. placeholder's ends instead
with: "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 not
think 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 parses success=true and is silently stripped, which is
precisely 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 exact
pre-change spelling min: z.number().optional().describe('Minimum value') — mutating the
fact, 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 the
mirror's shape …" (describe reverts to 'Minimum value'), with max / step /
placeholder, the live-input control, the inputType fork pin and the strip contrast all
staying 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), so
vitest loads the mutated src directly and a stale dist cannot hide the mutation.

Observed — matches the prediction:

 × refuses `min`, names it in the path, and answers with its own guidance
× the four stay in the mirror's shape — a tombstone is DECLARED, just unwritable
AssertionError: min: expected true to be false // Object.is equality
AssertionError: expected 'Minimum value' to contain 'RETIRED (objectui#5905)'
Test Files 1 failed (1)
Tests 2 failed | 9 passed (11)

(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 text
count 1, remaining tombstone calls 4 (1 import + 3 keys), git hash-object
c82523ed…622c6d39….

Restore proven BOTH ways, under trap … EXIT INT TERM, absolute paths from
git rev-parse --show-toplevel, with git checkout HEAD -- ABSOLUTE_PATH (never the bare
form, which reads from the index): git diff HEAD empty and each file's
git hash-object equal to its HEAD blob, for all four touched paths. Then a
post-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:

gateverdict lineexit
pnpm --filter @object-ui/types buildtscBUILD_EXIT=0
pnpm --filter @object-ui/types type-check (hyphenated)tsc --noEmit then tsc -p tsconfig.examples.json then tsc -p tsconfig.test.jsonTYPECHECK_EXIT=0
pnpm exec vitest run packages/types/ (root form)Test Files 76 passed (76) · Tests 872 passed (872)0
pnpm exec vitest run packages/core/src/registry/Test Files 7 passed (7) · Tests 111 passed (111)0
pnpm exec vitest run packages/sdui-parser/Test Files 11 passed (11) · Tests 147 passed (147)0
pnpm exec eslint . (plain form, no --no-inline-config)✖ 11516 problems (0 errors, 11516 warnings)ESLINT_EXIT=0
node scripts/check-changeset-presence.mjs✅ 4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)0
zod-mirror-parityTest Files 1 passed (1) · Tests 5 passed (5)0

vitest file/test counts, before → after (the wrong-cwd false green this repo has been
bitten by would report apps/console's 22 files instead):

Test FilesTests
BEFORE (merge-base state of packages/types)75861
AFTER76872
delta+1+11

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 at b03ba3ad5. The
+1 file / +11 tests are exactly the new pin file.

zod-mirror-parity deserves its own note since this removes declarations it pairs: it
compares the mirror's .shape against the declaration over the INTERSECTION of the mirror's
keys 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#ComponentInputSchema has no
KnownDrift and no UnmirroredDeclared entry before or after — same precedent as
DashboardComponentSchema.aria (objectui#5855).

eslint reports 0 errors; the 11,516 warnings are the repo's pre-existing
no-explicit-any population. The four files this PR touches contribute zero new
findings
(the base.ts / base.zod.ts warnings are on untouched lines).

10. What is NOT claimed

  • Not claimed: that nothing outside this repository writes these keys. Not measurable here.
  • Not claimed: that inputType should stay. It needs a ruling; this PR does not make it.
  • Not claimed: that constraint slots on ComponentInput are wrong. See §4.
  • Not run locally: the full repo test farm and the rest of the check:* set — CI owns
    those and runs them exactly once on this branch.

os-samand others added 2 commits August 31, 2026 02:35
`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
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3179.9 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-DkRl-aik.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 03:28
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 453dbaaAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-5905-componentinput-retire-five-keys branch August 31, 2026 03:42
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@os-sam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(types)!: retire ComponentInput's four inert constraint keys, and report the inputType fork - #6949

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-5905-componentinput-retire-five-keys
Aug 31, 2026
Merged

feat(types)!: retire ComponentInput's four inert constraint keys, and report the inputType fork#6949
os-sam merged 2 commits into
mainfrom
claude/issue-5905-componentinput-retire-five-keys

Conversation

@os-sam

@os-samos-sam commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Refs #5905. ⚠️Refs, not a closing keyword — the card asked for FIVE keys and this
retires FOUR.
The fifth, inputType, hit the fork condition triage set, and the card
stays open on that fork.

needs:contract-review tier: this changes members on a published type
(@object-ui/typesComponentInput). ⛔ Draft on purpose — not ready, not enqueued, no
auto-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:

keysitewhat it writes
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) takes
meta as ComponentMeta (packages/core/src/registry/Registry.ts:352), whose inputs is
ComponentInput[]. It is also pinnedpackages/plugin-markdown/src/index.test.ts:51
asserts expect(contentInput?.inputType).toBe('textarea').

So inputType is 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. inputType is left live and
writable
, 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, and
are 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, all
top-level directories, packages/ and apps/ and examples/ and e2e/ and content/
and skills/ and scripts/ included.
Not a package subset.

Instrument (structural, not a bare grep): bracket-match every inputs: array in every
tracked .ts/.tsx/.js/.jsx/.mjs/.cjs/.json/.md/.mdx file, then collect the top-level keys
of the object literals inside — i.e. the ComponentInput's own keys. 219 regions
scanned.

The zero comes with a HOT CONTROL, from the same pass over the same regions (not
merely repo-wide):

retired candidatesitescontrol: a key the serializer DOES forwardsites
inputType1name926
min0type926
max0description161
step0enum114
placeholder0required87

The instrument was demonstrably not blind: it found the one inputType site and four
digits of control hits in the same regions.

Blind spot found and closed by hand. The inputs: scan cannot see a ComponentInput[]
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) and ELEMENT_DATA_SOURCE_INPUT
(packages/core/src/data-scope/element-data-source.ts:327).

placeholder false positives, rejected BY HAND — and that rejection is a finding about
the instrument, not noise.
A plain placeholder: grep over the same file set returns 26
hits and none of them is a ComponentInput key:

  • packages/vscode-extension/src/providers/PreviewProvider.ts:180,193 — writes
    input.placeholder / textarea.placeholder on a DOM element, from
    BaseSchema.placeholder. Different type on both ends.
  • packages/app-shell/src/views/metadata-admin/previews/block-config.ts — an inspector
    field spec whose own key is named placeholder
    (a PlaceholderSpec). Different type.
  • Eight renderer registrations (combobox, command, date-picker, select, textarea,
    and three in plugin-chatbot) write placeholder in defaultProps, the element's
    own prop — while their inputs: array carries { name: 'placeholder', … }, i.e.
    placeholder as the value of name, never as a ComponentInput key.
  • plugin-designer / ObjectManager / plugin-view / form.tsx hits are form field
    descriptors (type: 'text', type: 'input' — neither is a ComponentInputControlType).

The publication path, re-read on the merge-base rather than trusted from the card.
packages/sdui-parser/src/index.ts:153-160 forwards exactly six keys per input:

inputs: (c.inputs??[]).map((i)=>({name: i.name,type: canonicalizeInputType(i.type),required: i.required,enum: i.enum,binding: i.binding,description: i.description,})),

None of the five is in it, so a value authored here could not reach the published
sdui.manifest.json even in principle.

Reads: none. A member-access census across the 71 files that consume .inputs returns
nothing for min / max / step / placeholder on a ComponentInput value (the hits are
i18n key strings, sim.step(), and element:text_input.placeholder, which is a different
surface). Same pass, same file set, control: .name 188, .type 302, .description 68,
.enum 28, .required 23.

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:
ComponentInputSchema is a non-strict z.object, so a deleted key would be silently
stripped
— 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 type field carries a maintainer ruling of 2026-08-17, quoted in
base.ts:

Ruled (maintainer, 2026-08-17): the coarse arm plus description IS the publication
face's expression ceiling today, and SPEC IS THE SOLE JUDGE OF VALUES.

Two directions were deferred, not rejected on merit: giving ComponentInput real
constraint slots (two sources of truth, free to drift), and binding checkType to spec's
Zod member when a ComponentPropsMap entry exists (one truth, but couples sdui-parser
to spec). The ruling names the reopen condition: a measured case of an author — human
or agent — shipping a spec-rejected value that objectui's silence let through.

min / max / step read exactly like the slots that ruling declined to add. What is
retired 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

Considered and deliberately not touched: packages/core/src/registry/Registry.ts's
doc 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/docs page and no published skills file teaches
these four on ComponentInput — the skill's own ComponentInput type block
(skills/objectui/guides/plugin-development.md:87) already lists eight keys and none of the
five. This diff does not touch the published skills directory at all.

6. ⚠️ The emitted .d.ts, measured on BOTH sides — what the contract reviewer needs

Built with pnpm --filter @object-ui/types build on each side, then diffed.

packages/types/dist/base.d.tsinterface ComponentInput (comments stripped):

 description?: string;
advanced?: boolean;
inputType?: string;
- min?: number;- max?: number;- step?: number;- placeholder?: string;+ min?: never;+ max?: never;+ step?: never;+ placeholder?: never;
}

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 / string to never. That is the tombstone contract: the key stays visible so an
author 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.
⚠️ The angle-bracket generic spelling is written out in words below on purpose: this
body's sanitizer eats tag-shaped fragments, and on the first publish it ate exactly this
table — both columns came back reading z.ZodOptional and the diff was therefore
meaningless. Read "OPT of X" as z.ZodOptional parameterised by X.

memberBEFOREAFTER
minOPT of z.ZodNumberOPT of z.ZodNever
maxOPT of z.ZodNumberOPT of z.ZodNever
stepOPT of z.ZodNumberOPT of z.ZodNever
placeholderOPT of z.ZodStringOPT of z.ZodNever

That shape is inlined three times in that one file — ComponentInputSchema,
ComponentMetaSchema and ComponentConfigSchema all carry it.

Which published subpaths carry the change (from package.jsonexports):

subpathfilehow it carries it
@object-ui/types/basedist/base.d.tsthe declaration itself
@object-ui/typesdist/index.d.tsre-exports the type by name (ComponentInput)
@object-ui/typesdist/index.d.tsre-exports it again as the deprecated alias PluginComponentInput (via dist/plugin-scope.d.ts)
@object-ui/types/zoddist/zod/index.zod.d.tsdist/zod/base.zod.d.tsComponentInputSchema, ComponentMetaSchema, ComponentConfigSchema

dist/widget.d.ts mentions ComponentInput only in prose and imports
ComponentInputControlType; WidgetInput's own eight-key shape is unchanged.

7. The refusal is real — before/after safeParse, with a control both runs

Same probe, same document shape, run against the mirror before and after the change.
Control document: { name: 'content', type: 'string', label: …, required: true }.

key writtenBEFOREAFTER
min: 0success=true, value KEPTsuccess=false, path ["min"], code invalid_type
max: 100success=true, value KEPTsuccess=false, path ["max"], code invalid_type
step: 1success=true, value KEPTsuccess=false, path ["step"], code invalid_type
placeholder: 'Type here…'success=true, value KEPTsuccess=false, path ["placeholder"], code invalid_type
inputType: 'textarea' (the fork)success=true, value KEPTsuccess=true, value KEPT — unchanged on purpose
CONTROL description: 'Help text'success=truesuccess=true
CONTROL bare documentsuccess=truesuccess=true

message per key, AFTER (this is the .describe() text too — one string, both channels):

RETIRED (objectui#5905) — ComponentInput.min was never read, and never published: the
manifest serializer forwards name/type/required/enum/binding/description and
this is not one of them, so an authored value was silently dropped. Delete the key; spell
the numeric domain out in description, which IS published.

max and step are the same sentence with their own key. placeholder's ends instead
with: "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 not
think 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 parses success=true and is silently stripped, which is
precisely 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 exact
pre-change spelling min: z.number().optional().describe('Minimum value') — mutating the
fact, 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 the
mirror's shape …" (describe reverts to 'Minimum value'), with max / step /
placeholder, the live-input control, the inputType fork pin and the strip contrast all
staying 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), so
vitest loads the mutated src directly and a stale dist cannot hide the mutation.

Observed — matches the prediction:

 × refuses `min`, names it in the path, and answers with its own guidance
× the four stay in the mirror's shape — a tombstone is DECLARED, just unwritable
AssertionError: min: expected true to be false // Object.is equality
AssertionError: expected 'Minimum value' to contain 'RETIRED (objectui#5905)'
Test Files 1 failed (1)
Tests 2 failed | 9 passed (11)

(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 text
count 1, remaining tombstone calls 4 (1 import + 3 keys), git hash-object
c82523ed…622c6d39….

Restore proven BOTH ways, under trap … EXIT INT TERM, absolute paths from
git rev-parse --show-toplevel, with git checkout HEAD -- ABSOLUTE_PATH (never the bare
form, which reads from the index): git diff HEAD empty and each file's
git hash-object equal to its HEAD blob, for all four touched paths. Then a
post-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:

gateverdict lineexit
pnpm --filter @object-ui/types buildtscBUILD_EXIT=0
pnpm --filter @object-ui/types type-check (hyphenated)tsc --noEmit then tsc -p tsconfig.examples.json then tsc -p tsconfig.test.jsonTYPECHECK_EXIT=0
pnpm exec vitest run packages/types/ (root form)Test Files 76 passed (76) · Tests 872 passed (872)0
pnpm exec vitest run packages/core/src/registry/Test Files 7 passed (7) · Tests 111 passed (111)0
pnpm exec vitest run packages/sdui-parser/Test Files 11 passed (11) · Tests 147 passed (147)0
pnpm exec eslint . (plain form, no --no-inline-config)✖ 11516 problems (0 errors, 11516 warnings)ESLINT_EXIT=0
node scripts/check-changeset-presence.mjs✅ 4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)0
zod-mirror-parityTest Files 1 passed (1) · Tests 5 passed (5)0

vitest file/test counts, before → after (the wrong-cwd false green this repo has been
bitten by would report apps/console's 22 files instead):

Test FilesTests
BEFORE (merge-base state of packages/types)75861
AFTER76872
delta+1+11

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 at b03ba3ad5. The
+1 file / +11 tests are exactly the new pin file.

zod-mirror-parity deserves its own note since this removes declarations it pairs: it
compares the mirror's .shape against the declaration over the INTERSECTION of the mirror's
keys 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#ComponentInputSchema has no
KnownDrift and no UnmirroredDeclared entry before or after — same precedent as
DashboardComponentSchema.aria (objectui#5855).

eslint reports 0 errors; the 11,516 warnings are the repo's pre-existing
no-explicit-any population. The four files this PR touches contribute zero new
findings
(the base.ts / base.zod.ts warnings are on untouched lines).

10. What is NOT claimed

  • Not claimed: that nothing outside this repository writes these keys. Not measurable here.
  • Not claimed: that inputType should stay. It needs a ruling; this PR does not make it.
  • Not claimed: that constraint slots on ComponentInput are wrong. See §4.
  • Not run locally: the full repo test farm and the rest of the check:* set — CI owns
    those and runs them exactly once on this branch.

os-samand others added 2 commits August 31, 2026 02:35
`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
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3179.9 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-DkRl-aik.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 03:28
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 453dbaaAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-5905-componentinput-retire-five-keys branch August 31, 2026 03:42
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@os-sam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(types)!: retire ComponentInput's four inert constraint keys, and report the inputType fork - #6949

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-5905-componentinput-retire-five-keys
Aug 31, 2026
Merged

feat(types)!: retire ComponentInput's four inert constraint keys, and report the inputType fork#6949
os-sam merged 2 commits into
mainfrom
claude/issue-5905-componentinput-retire-five-keys

Conversation

@os-sam

@os-samos-sam commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Refs #5905. ⚠️Refs, not a closing keyword — the card asked for FIVE keys and this
retires FOUR.
The fifth, inputType, hit the fork condition triage set, and the card
stays open on that fork.

needs:contract-review tier: this changes members on a published type
(@object-ui/typesComponentInput). ⛔ Draft on purpose — not ready, not enqueued, no
auto-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:

keysitewhat it writes
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) takes
meta as ComponentMeta (packages/core/src/registry/Registry.ts:352), whose inputs is
ComponentInput[]. It is also pinnedpackages/plugin-markdown/src/index.test.ts:51
asserts expect(contentInput?.inputType).toBe('textarea').

So inputType is 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. inputType is left live and
writable
, 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, and
are 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, all
top-level directories, packages/ and apps/ and examples/ and e2e/ and content/
and skills/ and scripts/ included.
Not a package subset.

Instrument (structural, not a bare grep): bracket-match every inputs: array in every
tracked .ts/.tsx/.js/.jsx/.mjs/.cjs/.json/.md/.mdx file, then collect the top-level keys
of the object literals inside — i.e. the ComponentInput's own keys. 219 regions
scanned.

The zero comes with a HOT CONTROL, from the same pass over the same regions (not
merely repo-wide):

retired candidatesitescontrol: a key the serializer DOES forwardsites
inputType1name926
min0type926
max0description161
step0enum114
placeholder0required87

The instrument was demonstrably not blind: it found the one inputType site and four
digits of control hits in the same regions.

Blind spot found and closed by hand. The inputs: scan cannot see a ComponentInput[]
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) and ELEMENT_DATA_SOURCE_INPUT
(packages/core/src/data-scope/element-data-source.ts:327).

placeholder false positives, rejected BY HAND — and that rejection is a finding about
the instrument, not noise.
A plain placeholder: grep over the same file set returns 26
hits and none of them is a ComponentInput key:

  • packages/vscode-extension/src/providers/PreviewProvider.ts:180,193 — writes
    input.placeholder / textarea.placeholder on a DOM element, from
    BaseSchema.placeholder. Different type on both ends.
  • packages/app-shell/src/views/metadata-admin/previews/block-config.ts — an inspector
    field spec whose own key is named placeholder
    (a PlaceholderSpec). Different type.
  • Eight renderer registrations (combobox, command, date-picker, select, textarea,
    and three in plugin-chatbot) write placeholder in defaultProps, the element's
    own prop — while their inputs: array carries { name: 'placeholder', … }, i.e.
    placeholder as the value of name, never as a ComponentInput key.
  • plugin-designer / ObjectManager / plugin-view / form.tsx hits are form field
    descriptors (type: 'text', type: 'input' — neither is a ComponentInputControlType).

The publication path, re-read on the merge-base rather than trusted from the card.
packages/sdui-parser/src/index.ts:153-160 forwards exactly six keys per input:

inputs: (c.inputs??[]).map((i)=>({name: i.name,type: canonicalizeInputType(i.type),required: i.required,enum: i.enum,binding: i.binding,description: i.description,})),

None of the five is in it, so a value authored here could not reach the published
sdui.manifest.json even in principle.

Reads: none. A member-access census across the 71 files that consume .inputs returns
nothing for min / max / step / placeholder on a ComponentInput value (the hits are
i18n key strings, sim.step(), and element:text_input.placeholder, which is a different
surface). Same pass, same file set, control: .name 188, .type 302, .description 68,
.enum 28, .required 23.

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:
ComponentInputSchema is a non-strict z.object, so a deleted key would be silently
stripped
— 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 type field carries a maintainer ruling of 2026-08-17, quoted in
base.ts:

Ruled (maintainer, 2026-08-17): the coarse arm plus description IS the publication
face's expression ceiling today, and SPEC IS THE SOLE JUDGE OF VALUES.

Two directions were deferred, not rejected on merit: giving ComponentInput real
constraint slots (two sources of truth, free to drift), and binding checkType to spec's
Zod member when a ComponentPropsMap entry exists (one truth, but couples sdui-parser
to spec). The ruling names the reopen condition: a measured case of an author — human
or agent — shipping a spec-rejected value that objectui's silence let through.

min / max / step read exactly like the slots that ruling declined to add. What is
retired 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

Considered and deliberately not touched: packages/core/src/registry/Registry.ts's
doc 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/docs page and no published skills file teaches
these four on ComponentInput — the skill's own ComponentInput type block
(skills/objectui/guides/plugin-development.md:87) already lists eight keys and none of the
five. This diff does not touch the published skills directory at all.

6. ⚠️ The emitted .d.ts, measured on BOTH sides — what the contract reviewer needs

Built with pnpm --filter @object-ui/types build on each side, then diffed.

packages/types/dist/base.d.tsinterface ComponentInput (comments stripped):

 description?: string;
advanced?: boolean;
inputType?: string;
- min?: number;- max?: number;- step?: number;- placeholder?: string;+ min?: never;+ max?: never;+ step?: never;+ placeholder?: never;
}

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 / string to never. That is the tombstone contract: the key stays visible so an
author 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.
⚠️ The angle-bracket generic spelling is written out in words below on purpose: this
body's sanitizer eats tag-shaped fragments, and on the first publish it ate exactly this
table — both columns came back reading z.ZodOptional and the diff was therefore
meaningless. Read "OPT of X" as z.ZodOptional parameterised by X.

memberBEFOREAFTER
minOPT of z.ZodNumberOPT of z.ZodNever
maxOPT of z.ZodNumberOPT of z.ZodNever
stepOPT of z.ZodNumberOPT of z.ZodNever
placeholderOPT of z.ZodStringOPT of z.ZodNever

That shape is inlined three times in that one file — ComponentInputSchema,
ComponentMetaSchema and ComponentConfigSchema all carry it.

Which published subpaths carry the change (from package.jsonexports):

subpathfilehow it carries it
@object-ui/types/basedist/base.d.tsthe declaration itself
@object-ui/typesdist/index.d.tsre-exports the type by name (ComponentInput)
@object-ui/typesdist/index.d.tsre-exports it again as the deprecated alias PluginComponentInput (via dist/plugin-scope.d.ts)
@object-ui/types/zoddist/zod/index.zod.d.tsdist/zod/base.zod.d.tsComponentInputSchema, ComponentMetaSchema, ComponentConfigSchema

dist/widget.d.ts mentions ComponentInput only in prose and imports
ComponentInputControlType; WidgetInput's own eight-key shape is unchanged.

7. The refusal is real — before/after safeParse, with a control both runs

Same probe, same document shape, run against the mirror before and after the change.
Control document: { name: 'content', type: 'string', label: …, required: true }.

key writtenBEFOREAFTER
min: 0success=true, value KEPTsuccess=false, path ["min"], code invalid_type
max: 100success=true, value KEPTsuccess=false, path ["max"], code invalid_type
step: 1success=true, value KEPTsuccess=false, path ["step"], code invalid_type
placeholder: 'Type here…'success=true, value KEPTsuccess=false, path ["placeholder"], code invalid_type
inputType: 'textarea' (the fork)success=true, value KEPTsuccess=true, value KEPT — unchanged on purpose
CONTROL description: 'Help text'success=truesuccess=true
CONTROL bare documentsuccess=truesuccess=true

message per key, AFTER (this is the .describe() text too — one string, both channels):

RETIRED (objectui#5905) — ComponentInput.min was never read, and never published: the
manifest serializer forwards name/type/required/enum/binding/description and
this is not one of them, so an authored value was silently dropped. Delete the key; spell
the numeric domain out in description, which IS published.

max and step are the same sentence with their own key. placeholder's ends instead
with: "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 not
think 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 parses success=true and is silently stripped, which is
precisely 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 exact
pre-change spelling min: z.number().optional().describe('Minimum value') — mutating the
fact, 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 the
mirror's shape …" (describe reverts to 'Minimum value'), with max / step /
placeholder, the live-input control, the inputType fork pin and the strip contrast all
staying 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), so
vitest loads the mutated src directly and a stale dist cannot hide the mutation.

Observed — matches the prediction:

 × refuses `min`, names it in the path, and answers with its own guidance
× the four stay in the mirror's shape — a tombstone is DECLARED, just unwritable
AssertionError: min: expected true to be false // Object.is equality
AssertionError: expected 'Minimum value' to contain 'RETIRED (objectui#5905)'
Test Files 1 failed (1)
Tests 2 failed | 9 passed (11)

(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 text
count 1, remaining tombstone calls 4 (1 import + 3 keys), git hash-object
c82523ed…622c6d39….

Restore proven BOTH ways, under trap … EXIT INT TERM, absolute paths from
git rev-parse --show-toplevel, with git checkout HEAD -- ABSOLUTE_PATH (never the bare
form, which reads from the index): git diff HEAD empty and each file's
git hash-object equal to its HEAD blob, for all four touched paths. Then a
post-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:

gateverdict lineexit
pnpm --filter @object-ui/types buildtscBUILD_EXIT=0
pnpm --filter @object-ui/types type-check (hyphenated)tsc --noEmit then tsc -p tsconfig.examples.json then tsc -p tsconfig.test.jsonTYPECHECK_EXIT=0
pnpm exec vitest run packages/types/ (root form)Test Files 76 passed (76) · Tests 872 passed (872)0
pnpm exec vitest run packages/core/src/registry/Test Files 7 passed (7) · Tests 111 passed (111)0
pnpm exec vitest run packages/sdui-parser/Test Files 11 passed (11) · Tests 147 passed (147)0
pnpm exec eslint . (plain form, no --no-inline-config)✖ 11516 problems (0 errors, 11516 warnings)ESLINT_EXIT=0
node scripts/check-changeset-presence.mjs✅ 4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)0
zod-mirror-parityTest Files 1 passed (1) · Tests 5 passed (5)0

vitest file/test counts, before → after (the wrong-cwd false green this repo has been
bitten by would report apps/console's 22 files instead):

Test FilesTests
BEFORE (merge-base state of packages/types)75861
AFTER76872
delta+1+11

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 at b03ba3ad5. The
+1 file / +11 tests are exactly the new pin file.

zod-mirror-parity deserves its own note since this removes declarations it pairs: it
compares the mirror's .shape against the declaration over the INTERSECTION of the mirror's
keys 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#ComponentInputSchema has no
KnownDrift and no UnmirroredDeclared entry before or after — same precedent as
DashboardComponentSchema.aria (objectui#5855).

eslint reports 0 errors; the 11,516 warnings are the repo's pre-existing
no-explicit-any population. The four files this PR touches contribute zero new
findings
(the base.ts / base.zod.ts warnings are on untouched lines).

10. What is NOT claimed

  • Not claimed: that nothing outside this repository writes these keys. Not measurable here.
  • Not claimed: that inputType should stay. It needs a ruling; this PR does not make it.
  • Not claimed: that constraint slots on ComponentInput are wrong. See §4.
  • Not run locally: the full repo test farm and the rest of the check:* set — CI owns
    those and runs them exactly once on this branch.

os-samand others added 2 commits August 31, 2026 02:35
`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
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3179.9 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-DkRl-aik.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 03:28
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit 453dbaaAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-5905-componentinput-retire-five-keys branch August 31, 2026 03:42
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@os-sam