Skip to content

feat(types,components): declare and draw input-otp's separator (objectui#11365) - #11469

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-11365-input-otp-separator
Oct 2, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-11365-input-otp-separator

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11365

Clause-②: yes

What this changes

Triage ruled the enforce arm of ADR-0049 (answer 5943561653 on the card). The shipped primitive already exports InputOTPSeparator (role="separator"), so the key is declared and drawn. The docs page and both catalog entries stay as they are, and they are now valid.

  • Types (both faces). InputOTPSchema gains separator?: boolean on the TypeScript face (packages/types/src/form.ts) and on the zod mirror (packages/types/src/zod/form.zod.ts). The node's body / children refusal messages list what the node renders instead, and that list now names separator.
  • Renderer. With separator: true, packages/components/src/renderers/form/input-otp.tsx splits the length slots into two InputOTPGroups at the midpoint and draws one InputOTPSeparator between them, the shadcn "with separator" layout.
    • Odd length: the extra slot goes in the first group, so 5 slots draw as 3 and 2, and 7 as 4 and 3.
    • length: 1: one slot has nothing to separate, so it draws no separator.
    • Absent or false: one group holding every slot, byte-identical to before (proof below).
  • Registration. The input-otp registration publishes { name: 'separator', type: 'boolean' } in inputs.
  • Changeset. minor for @object-ui/types and @object-ui/components: the published face gains a member.

Accept-set change: before and after

Before is BASE 5988b6b53; after is head da4d38ef.

Reading Before After
components-form-input-otp/with-visual-separator through the real SchemaRenderer: role="separator" count 0 1
components-form-input-otp/verification-form (its input-otp sits at properties.children.1 of a flex bag): role="separator" count 0 1
with-visual-separator slot groups one group of 6 3 and 3
components-form-input-otp/6-digit-otp (no separator) no separator, one group of 6 unchanged
safeValidateSchema on both authoring entries green, because .passthrough() left the key unjudged green, and the key is judged
Strict authoring face (StrictAnyComponentSchema) on with-visual-separator unrecognized_keys at the root, key separator green
Strict authoring face on verification-form unrecognized_keys at properties.children.1, key separator green
{ type: 'input-otp', separator: 'yes' } under safeValidateSchema green (passthrough) invalid_type at separator
The same value on an input-otp inside a flex bag green invalid_type at properties.children.0.separator
TS face: an InputOTPSchema literal with separator: 'yes' compiles: BaseSchema's index signature is any compile error, pinned by a @ts-expect-error
html/jsx page tier: an input-otp tag written with separator={true} unknown-prop warning, "has no prop separator" no diagnostic

The html/jsx tier row was measured once with a throwaway probe. It ran compile with the same manifestFromConfigs mapping as page.tsx's getJsxManifest, on head and on head with only the inputs row removed. BASE has the same inputs. No gate requires the row: check:component-surface-parity is report-only and inputs stays inside its union either way, and registry-inputs-spec-parity covers only types with a spec row, which input-otp lacks. The row is there so that tier stops warning off a key the renderer honours.

Corpus re-judged

Every document that authors input-otp:

  • Catalog, all five examples/schema-catalog/src/schemas/components-form-input-otp/* entries:
    • with-visual-separator and verification-form author separator: true. Both are now drawn, and both are green on both faces (table above).
    • 4-digit, 6-digit-otp and 8-digit do not author it. Their rendered markup is byte-identical to BASE (proof below).
  • Docs page content/docs/components/form/input-otp.mdx: its interface block lists separator?: boolean; // Show visual separator, which is now true. The page is unedited.
  • The installed @objectstack/spec 17.5.0 has no input-otp row, so there is nothing to conflict with:
    • a recursive grep of its dist/ for input-otp, InputOTP and their spellings exits 1, while a control grep for 'input' under dist/ui exits 0;
    • separator has 0 hits in dist/ui/index.d.mts.
  • objectstack (1d0600bf) authors no input-otp anywhere: git grep -c input-otp exits 1, and a control grep in packages/spec/src/ui exits 0.

Pins

  • examples/schema-catalog/test/input-otp-separator-11365.test.tsx renders through the real SchemaRenderer and checks:

    • both authoring entries draw exactly one role="separator";
    • the 3 and 3 split;
    • an odd length gives 3 and 2, and one slot draws no separator;
    • the control: 6-digit-otp draws none, in one group;
    • separator: false gives the same innerHTML as no separator at all;
    • the registration publishes the boolean input.

    It also checks parsing: safeValidateSchema and the strict face are green on both entries, and a non-boolean separator is refused with invalid_type at its path, flat and nested in a bag.

  • packages/types/src/__tests__/input-otp-separator-11365.test.ts:

    • compile-time: InputOTPSchema['separator'] is exactly boolean | undefined, the declared spelling compiles, and separator: 'yes' is a @ts-expect-error;
    • runtime: the mirror keeps a boolean separator, and refuses a string with invalid_type at separator. A control shows an undeclared key still passes through.

Reverse verification (each run from a committed head)

  • Red first. The catalog pin was run on the untouched BASE tree before any source edit. Both authoring entries read 0 role="separator". The strict face reported unrecognized_keys for separator, at the root for with-visual-separator and at properties.children.1 for verification-form.
  • Ablation of the three source files. I checked out form.ts, zod/form.zod.ts and the renderer from BASE under a restore trap, then confirmed on disk that InputOTPSeparator was 0 in the renderer, separator?: boolean was 0 in form.ts and separator: z was 0 in the zod file.
    • vitest: 7 failed and 6 passed. The 6 passing were the controls, the passthrough rows, the boolean round-trip and the false equivalence.
    • tsc -p tsconfig.test.json for packages/types exited 2: TS2344 on the boolean | undefined equality, and TS2578, an unused @ts-expect-error.
    • Restored with git checkout HEAD -- .... git diff HEAD is 0 bytes, and every hash matches its HEAD blob.
  • Ablation of the inputs row. The row count read 0 on disk. The registration pin went red: 1 failed, 10 passed. Restored, and the hash matches HEAD.
  • Byte-identity without separator. I rendered 4-digit, 6-digit-otp and 8-digit through SchemaRenderer with the head renderer and with the BASE renderer. Both outputs are 6201 bytes and cmp exits 0.

Gates run locally

Node 22.23.3 was used, the latest 22.x that CI's 22.x resolves to; see the report for why.

Gate Result
pnpm --filter '@object-ui/components^...' build, then @object-ui/components itself exit 0
pnpm --filter @object-ui/types --filter @object-ui/components run type-check (both scripts echoed) exit 0
pnpm --filter @object-ui/example-schema-catalog run type-check, on the doc-gate build closure exit 0
pnpm exec vitest run packages/types/ 331 files, 8765 tests passed
pnpm exec vitest run packages/components/ examples/schema-catalog/ 391 passed, 1 skipped; 5808 tests passed, 24 skipped
pnpm exec vitest run apps/console/src/__tests__/registry-inputs-spec-parity.test.ts packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx (readers of inputs and of ui:input-otp) 2 files, 476 tests passed
New pins at head da4d38ef 2 files, 15 tests passed
check:doc-snippets, after the scoped --build-filter turbo build exit 0; 776 of 776 blocks judged, 0 failed
check:doc-examples exit 0
check:doc-types exit 0
check:component-surface-parity (report-only) input-otp: reads 3 to 4, interface 9 to 10, inputs 2 to 3. Findings stay at the same 2 (see Acceptance notes), and the full report is otherwise identical to BASE
node scripts/check-changeset-no-major.mjs, check:new-line-citations, check:control-bytes, check-changeset-presence exit 0 each, at head da4d38ef

Declared narrowing. check:sdui-registration-pins was not run locally:

  • It weighs a built console bundle for registrations the sideEffects arrays promise.
  • This diff adds and removes no registration and touches no sideEffects array.
  • CI's performance-budget.yml runs it.

The repo-wide lint and the full farm also belong to CI.

Acceptance notes

  • check:component-surface-parity still reports two older input-otp findings, unchanged by this PR: InputOTPSchema declares defaultValue and error, and the renderer reads neither by name. They belong to that census's family (objectui#4631). They are recorded here, not filed.
  • The docs page does not describe the midpoint split or the odd-length rule; the changeset and the type's docblock do. No sentence on the page became false, so the page is unedited.

Session: https://claude.ai/code/session_01XvhGmGAP79ZB8swnkapxPC


Generated by Claude Code

claude added 3 commits October 2, 2026 13:04
…w it (objectui#11365)

The docs page's "With Separator" example and two catalog entries author
`separator: true` on `input-otp`, but no type declared it and the renderer
read nothing for it: both entries rendered zero `role="separator"` elements
through the real SchemaRenderer. Triage ruled the enforce arm (ADR-0049).

- `InputOTPSchema.separator?: boolean` on the TS face and the zod mirror.
- The renderer splits the slots at the midpoint (odd length: the extra slot
  goes first) and draws one InputOTPSeparator between two groups. Absent or
  false keeps the one-group markup.
- The registration publishes `separator` as a boolean input.
- Pins: the catalog render/parse pin and the types face pin.

Claude-Session: https://claude.ai/code/session_01XvhGmGAP79ZB8swnkapxPC
Co-authored-by: Claude <noreply@anthropic.com>
…put (objectui#11365)

Without the boolean `separator` row in the `input-otp` registration's
`inputs`, the html/jsx page tier's whitelist answers `unknown-prop` for a
key the renderer honours (measured once with the row removed).

Claude-Session: https://claude.ai/code/session_01XvhGmGAP79ZB8swnkapxPC
Co-authored-by: Claude <noreply@anthropic.com>
…or layout (objectui#11365)

Claude-Session: https://claude.ai/code/session_01XvhGmGAP79ZB8swnkapxPC
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 17 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/5905-componentinput-inputtype-tombstone.md

  • names zod/form.zod.ts → packages/types/src/zod/form.zod.ts — edited by this change

    The write was measured as a no-op before it was deleted, and re-measured on this branch's base rather than inherited from the card. A structural census over every inputs: array in the repository (211 regions, all tracked TS/TSX/JS sources) scores inputType at exactly ONE authoring site — the plugin-markdown registration — against name 953, type 969, label 966, description 194, enum 119, required 86 and binding 4 in the same pass over the same regions, so the instrument was not blind. The other 192 in-repo inputType hits are a DIFFERENT face: FormField.inputType (zod/form.zod.ts), the text-input renderer's prop, and SchemaBuilder.inputType, none of which sit on a ComponentInput. The publication path is unchanged and was re-confirmed: packages/sdui-parser/src/index.ts forwards exactly seven keys per input — name, type, of, required, enum, binding, description — so an authored inputType could not reach the published sdui.manifest.json even in principle.

.changeset/6349-name-authority-batch-3.md

  • names form.zod.ts → packages/types/src/zod/form.zod.ts — edited by this change

    @object-ui/components — ComboboxOption now IS @object-ui/types' declaration. The component declared its own { value, label }, a strict subset of the ComboboxOption that @object-ui/types declares for ComboboxSchema.options and mirrors in form.zod.ts ({ value, label, disabled? }). The component now re-exports the types declaration (through the @object-ui/types/form subpath — the root barrel does not publish the name), so the name ComboboxOption exported from @object-ui/components gains the optional disabled?: boolean member. Every value that type-checked before still does — nothing narrows and no key changes type; the one thing that moves is keyof ComboboxOption, so a consumer that EXHAUSTS the type (a Record over its keys) will need the new key. Note that the Combobox component itself does not read option.disabled — that member was already declared on the @object-ui/types face and is now visible on this one too; it is recorded as a separate finding, not changed here.

.changeset/6396-previous-values-dom-leak.md

  • names packages/types/src/form.ts → packages/types/src/form.ts — edited by this change

    Scope is the runtime leak only. The declared key stays exactly as declared (packages/types/src/form.ts, packages/types/src/zod/form.zod.ts are untouched): it has a live consumer, so there is nothing here for the enforce-or-remove channel.

  • names packages/types/src/zod/form.zod.ts → packages/types/src/zod/form.zod.ts — edited by this change

    Scope is the runtime leak only. The declared key stays exactly as declared (packages/types/src/form.ts, packages/types/src/zod/form.zod.ts are untouched): it has a live consumer, so there is nothing here for the enforce-or-remove channel.

.changeset/6397-partial-schema-collapse-pin.md

  • names packages/types/src/form.ts → packages/types/src/form.ts — edited by this change

    ⚠️ Dated note, 2026-09-28 — a generic re-spelling of this Omit does not collapse — objectui#9256. Later in this same release objectui#9256 declared a key-remapping OmitDeclared ({ [P in keyof T as P extends K ? never : P]: T[P] }, in packages/types/src/form.ts) and re-spelled the heritage of InputShorthandSchema and UiCalendarSchema with it. Such a mapped type iterates the named members and the index signature separately, so it drops exactly K, keeps every other named member with its modifiers, and keeps the index signature. The same spelling of this alias declares 28 properties at ButtonSchema and 62 at ObjectGridSchema in this release, where the shipped alias declares 1 at each; at this change's own commit, under the same TypeScript, it declares 27 and 61, the source counts in the table above. So "every generic re-spelling collapses" was not true when it was written. The alias is still left as written: re-spelling it would narrow what every instantiation accepts, a contract change and not a pin's business. The PartialSchema docblock and partial-schema-collapse-pin.test.ts carry the correction. The rest of this entry is kept as the reading of this change.

.changeset/6938-checkbox-wrapper-class.md

  • names packages/types/src/form.ts → packages/types/src/form.ts — edited by this change

    packages/components/src/renderers/form/checkbox.tsx:36 reads cn("flex items-center space-x-2", schema.wrapperClass) — classes on the wrapper div around the box and its label — and neither the TypeScript interface in packages/types/src/form.ts nor the zod mirror in zod/form.zod.ts declared the key. It compiled through BaseSchema's index signature and parsed through .passthrough(), admitted unexamined. The same key, on the same class of read, is declared on FileUploadSchema and FilterBuilderSchema (objectui#6150); the checkbox was left out only because its doc page's schema block is a six-line summary.

  • names zod/form.zod.ts → packages/types/src/zod/form.zod.ts — edited by this change

    packages/components/src/renderers/form/checkbox.tsx:36 reads cn("flex items-center space-x-2", schema.wrapperClass) — classes on the wrapper div around the box and its label — and neither the TypeScript interface in packages/types/src/form.ts nor the zod mirror in zod/form.zod.ts declared the key. It compiled through BaseSchema's index signature and parsed through .passthrough(), admitted unexamined. The same key, on the same class of read, is declared on FileUploadSchema and FilterBuilderSchema (objectui#6150); the checkbox was left out only because its doc page's schema block is a six-line summary.

.changeset/7113-chart-data-model.md

  • names form.zod.ts → packages/types/src/zod/form.zod.ts — edited by this change

    .extend() with a NEW key still works and preserves the fold and the refinement; .optional(), z.discriminatedUnion, z.toJSONSchema and safeValidateSchema are all unaffected. Nothing in this repository calls the throwing combinators on either const, and the published surface already ships refined mirrors (objectql.zod.ts, complex.zod.ts, form.zod.ts, app.zod.ts), so the class is not new — but it is a real behaviour change on a published export and it belongs in the release note rather than in a reviewer's file.

.changeset/7530-predicate-envelope-declared.md

  • names zod/form.zod.ts → packages/types/src/zod/form.zod.ts — edited by this change

    • ExpressionWire (type, main entry) — the TypeScript wire union, in packages/types/src/expression.ts. - ExpressionWireSchema (@object-ui/types/zod) — its runtime twin, hoisted out of zod/form.zod.ts (where it was module-private) into zod/expression.zod.ts and imported by both base.zod.ts and form.zod.ts. One envelope type, reused by reference; no second spelling.
  • names form.zod.ts → packages/types/src/zod/form.zod.ts — edited by this change

    • ExpressionWire (type, main entry) — the TypeScript wire union, in packages/types/src/expression.ts. - ExpressionWireSchema (@object-ui/types/zod) — its runtime twin, hoisted out of zod/form.zod.ts (where it was module-private) into zod/expression.zod.ts and imported by both base.zod.ts and form.zod.ts. One envelope type, reused by reference; no second spelling.

.changeset/7697-combobox-option-root-barrel.md

  • names src/form.ts → packages/types/src/form.ts — edited by this change

    Additive only. ComboboxOption is added to the root barrel's existing named re-export list from ./form.js, next to the sibling option types that were already there (SelectOption, RadioOption). Nothing is removed, retyped or narrowed: the declaration stays in src/form.ts, its three members (value, label, disabled?) are unchanged, and the @object-ui/types/form subpath spelling keeps working exactly as before. Both spellings now resolve to the same declaration.

.changeset/7722-wrapper-class-five-more.md

  • names form.ts → packages/types/src/form.ts — edited by this change

    Each of renderers/form/switch.tsx, textarea.tsx, date-picker.tsx, select.tsx and renderers/data-display/list.tsx reads schema.wrapperClass onto its wrapper element, and neither the TypeScript interface (form.ts, data-display.ts) nor the zod mirror (zod/form.zod.ts, zod/data-display.zod.ts) declared the key. The reads compiled through BaseSchema's index signature (objectui#5155) and the values parsed through .passthrough(), admitted unexamined. The same key, on the same class of read, is declared on CheckboxSchema (b74a8598d), FileUploadSchema and FilterBuilderSchema (objectui#6150); these five were left out only because their doc pages never listed it.

  • names zod/form.zod.ts → packages/types/src/zod/form.zod.ts — edited by this change

    Each of renderers/form/switch.tsx, textarea.tsx, date-picker.tsx, select.tsx and renderers/data-display/list.tsx reads schema.wrapperClass onto its wrapper element, and neither the TypeScript interface (form.ts, data-display.ts) nor the zod mirror (zod/form.zod.ts, zod/data-display.zod.ts) declared the key. The reads compiled through BaseSchema's index signature (objectui#5155) and the values parsed through .passthrough(), admitted unexamined. The same key, on the same class of read, is declared on CheckboxSchema (b74a8598d), FileUploadSchema and FilterBuilderSchema (objectui#6150); these five were left out only because their doc pages never listed it.

.changeset/7735-zod-mirrors-stop-authoring-defaults.md

  • names form.zod.ts → packages/types/src/zod/form.zod.ts — edited by this change

    What changed. All 41 .default() call sites under packages/types/src/zod/ are removed — layout.zod.ts 22, crud.zod.ts 11, form.zod.ts 5, views.zod.ts 2, app.zod.ts 1. @object-ui/components reconciles the third face a separate finding found: flex's registration defaultProps.align seeded 'center', the value its own renderer never applies, so a designer-made node laid out differently from a hand-authored one; it now seeds 'start'.

.changeset/8072-input-wrapper-class-mirrored.md

  • names form.ts → packages/types/src/form.ts — edited by this change

    packages/components/src/renderers/form/input.tsx reads cn("grid w-full items-center gap-1.5", schema.wrapperClass) onto the wrapper div around the input and its label. The TypeScript face has declared the key all along (form.ts, docblock "Input wrapper CSS class"); the zod mirror never did, so the value rode through .passthrough() and { type: 'input', wrapperClass: 42 } validated GREEN — while the identical document on any of the other eight schema.wrapperClass readers (checkbox, file-upload, filter-builder — objectui#6150 / b74a8598d; switch, textarea, date-picker, select, list — objectui#7722) was refused at the key.

.changeset/8478-describe-line-addresses.md

.changeset/8478-zod-pins-form-layout.md

.changeset/8499-node-slot-registered-arms.md

  • names zod/form.zod.ts → packages/types/src/zod/form.zod.ts — edited by this change

    • SemanticElementSchema (zod/layout.zod.ts) — the seven HTML sectioning tags renderers/layout/semantic.tsx registers: aside main header nav footer section article. - HtmlElementSchema (zod/layout.zod.ts) — the 37 safe flow/inline tags renderers/basic/html-elements.tsx registers (h1…h6, p, a, ul, img, …), plus the per-tag keys that module forwards to the DOM (href, target, rel, title, src, alt, width, height, dateTime, cite). ⚠️ Dated note, 2026-09-27 — that set has since gained code — objectui#10756. At this change TAGS and this arm both named 37 tags; both now name 38, and the parity pin counts 38. The rest of this entry is kept as the reading of this change. - InputShorthandSchema (zod/form.zod.ts) — email / password, the two aliases renderers/form/input.tsx registers onto the input renderer with inputType pinned. inputType is deliberately NOT declared on this arm: the wrapper spreads its own value last, so an authored one is overwritten. ⚠️ Dated note, 2026-09-28 — inputType is now declared on this arm, as a refusal — objectui#8762. Later in this same release the arm declares inputType on both faces and refuses it by name (?: never on the TypeScript face, a retirementTombstone on the zod mirror, at path inputType), with guidance pointing at { "type": "input", "inputType": "email" }. So "inputType is deliberately NOT declared on this arm" no longer holds; the reason does, since the wrapper still spreads its own value last. The rest of this entry is kept as the reading of this change. - UiCalendarSchema (zod/form.zod.ts) — ui:calendar, the date-picker primitive renderers/form/calendar.tsx registers under exactly that key (skipFallback, because bare calendar belongs to the plugin-calendar view).

.changeset/9067-zod-barrel-named-arms.md

  • names zod/form.zod.ts → packages/types/src/zod/form.zod.ts — edited by this change

    • InputShorthandSchema (zod/form.zod.ts) — the email / password shorthand arm. - UiCalendarSchema (zod/form.zod.ts) — ui:calendar, the date-picker primitive renderers/form/calendar.tsx registers, a different component from the calendar plugin view that owns the bare literal.

.changeset/9406-types-root-barrel-two-names.md

  • names form.ts → packages/types/src/form.ts — edited by this change

    form-barrel-mirror-9406.test.ts keeps it closed, and it is DERIVED rather than a pair of presence assertions: it reads form.ts's export list and the root barrel's ./form.js re-export list on every run and names whatever is in the first and not the second. A pin asserting "these two names are present" would pass on the day the next declaration lands in form.ts and is forgotten, which is this class reopening yet again. Names deliberately left off the list get a ledger row carrying the reason instead, and a row goes red once its name reaches the barrel or stops being declared.

.changeset/9526-types-root-barrel-command-element-types.md

  • names form.ts → packages/types/src/form.ts — edited by this change

    The change is purely additive: the two names join the root barrel's existing named re-export list from ./form.js, beside CommandSchema. Nothing is removed, renamed or narrowed, the declarations stay in form.ts, and the /form subpath keeps working. It is the third instance of one class, repaired by the same route each time: objectui#7697 for ComboboxOption, and objectui#9406 (director decision batch 133, item 2, letter (a)) for InputShorthandSchema and UiCalendarSchema.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with 5988b6b53 (merge-base with origin/main): 5 file(s) changed outside .changeset/, read against 2014 pending declaration(s) that publish a body (2642 pending in total). · run

@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3551.3 KB 3574.6 KB
Main entry chunk (gzip) 150.4 KB 350 KB
Entry file index-CXkx7lLD.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.88KB 6.25KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.17KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 571.01KB 136.82KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 230.46KB 63.94KB
fields (index.js) 261.57KB 66.16KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.49KB 9.23KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 39.82KB 11.27KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 53.17KB 15.46KB
plugin-charts (index.js) 83.53KB 22.84KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 141.26KB 38.01KB
plugin-designer (index.js) 231.28KB 48.76KB
plugin-detail (index.js) 245.27KB 64.47KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 172.73KB 44.50KB
plugin-gantt (index.js) 179.14KB 45.06KB
plugin-grid (index.js) 232.66KB 63.77KB
plugin-kanban (index.js) 49.51KB 15.54KB
plugin-list (index.js) 116.60KB 28.96KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.04KB 12.21KB
plugin-timeline (index.js) 38.80KB 11.71KB
plugin-tree (index.js) 14.51KB 5.15KB
plugin-view (index.js) 90.23KB 22.73KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.06KB 2.68KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 21.42KB 7.05KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 3.19KB 1.62KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.07KB 2.39KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 21.59KB 7.71KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: da4d38ef042f771adc918f817aeae62986945c27
Local-runs: none

Inputs: card #11365 (body and all five comments, 5928214045 through the os-dev-report 5953952211), PR #11469 (body, six-file list, net diff against main 5988b6b), the check-runs on the head (three REST reads, the last at 2026-10-02T14:03:51Z), and REST reads of files AT THE HEAD wherever a claim in the diff needed a witness: the ui primitive and its barrel, the docs page, the two catalog entries, the zod barrel and strict-face module, the changeset config, both package manifests, the root vitest config, the catalog test directory, the full head tree. Nothing checked out, built, run or re-run.

① Derived judgments

Each accept-set or public-surface change the diff implies, with a verdict:

  1. TS face — InputOTPSchema.separator?: boolean in packages/types/src/form.ts (published .d.ts, subpaths . and ./form). RIGHT. The public door already taught exactly this spelling: the docs page at the head reads separator?: boolean; // Show visual separator, and triage ruled the enforce arm (5943561653). For this one key BaseSchema's [key: string]: any admission narrows to boolean | undefined; the types pin's @ts-expect-error row and Equal row carry that, and the head's Type Check check-run (which runs the package's tsc -p tsconfig.test.json leg) is green.
  2. Zod face — separator: z.boolean().optional() on the mirror in packages/types/src/zod/form.zod.ts. RIGHT, and the two faces agree in name and type, as triage required ("either way one declaration"). Two effects, both named in the PR and both pinned: the strict authoring face (StrictAnyComponentSchema, re-exported from @object-ui/types/zod out of strict-authoring-face.ts, confirmed at the head) now accepts the key where it refused unrecognized_keys; the passthrough face now refuses a non-boolean with invalid_type at the key, flat and nested at properties.children.0.separator, where it let it through unjudged.
  3. Renderer — packages/components/src/renderers/form/input-otp.tsx: separator === true with a length above 1 splits at ceil(length/2), draws one InputOTPSeparator between two InputOTPGroups. RIGHT. The primitive exists at the head in packages/components/src/ui/input-otp.tsx as a div with role="separator", and the barrel exports it (export * from './input-otp'); the diff imports it and touches no ui/** file, so the claim's AGENTS.md 构建一个 **Live Playground (实时演练场)** (用于展示引擎能力) #7 line held. The strict === true matches the boolean face. Control path: absent or false gives split === length, one group, slot keys 0..n-1 exactly as before, so the markup is unchanged by construction; the pin checks false against absent by innerHTML. The two rules the docs page does not state (odd length puts the extra slot first; one slot draws no separator) are documented in the docblock and the changeset and contradict nothing on the page.
  4. Registration — inputs gains { name: 'separator', type: 'boolean' }. RIGHT: inputs is the html/jsx page tier's prop whitelist, and the renderer now honours the key; the row is pinned through ComponentRegistry.getMeta. No gate required the row, which is the dev's finding and mine; the pin is what guards it.
  5. Tombstone text — the body / children refusal messages on both faces now list separator under "What it renders instead". RIGHT, message text only, changed in step on both faces.
  6. Docs page and catalog unchanged — RIGHT. At the head, with-visual-separator.json and the input-otp inside verification-form.json (at properties.children.1) both author length: 6, separator: true, so both now draw 3 and 3; no sentence on the page became false. Triage's three pins are covered: one role="separator" on the With Separator entry; the control without separator unchanged; both entries green under safeValidateSchema. The dev's note that the third was already green on BASE through .passthrough() is correct, and adding the strict-face pin (red on BASE) as the declaration's real evidence is the right reading of that pin.
  7. No stale ledger — the head tree holds no exemption or allowlist naming input-otp's separator (the catalog corpus tests' separator hits are MenuItem's divider arm), no generated JSON-schema corpus and no API report under packages/types that a new member would have to regenerate.
  8. Pins are CI-discoverable — the root vitest.config.mts includes examples/**/*.test.ts and examples/**/*.test.tsx, and the new catalog pin sits beside some forty sibling pins in examples/schema-catalog/test/; the types pin sits in src/__tests__/.

Nothing wrong found. The diff is six files, every one inside the claim's file surface (5952611327).

② Semver level

Changeset .changeset/11365-input-otp-separator.md: @object-ui/types: minor, @object-ui/components: minor. Both are 17.6.0 inside one fixed group in .changeset/config.json, so the group bumps together. RIGHT. The diff publishes a new optional member on a shipped .d.ts interface and on the mirror's .shape, a new renderer capability, and a new inputs row: additive, so minor, not patch. The one tightening (a non-boolean separator, formerly admitted by any and .passthrough() and rendering nothing, now refused by name) sits on a value the public door never admitted, and the repo's bump policy caps the level at minor (Changeset Bump Policy green; the dev's check-changeset-no-major exit 0). @object-ui/example-schema-catalog is in the config's ignore list; no changeset owed for the pin.

Clause-②: yes. The PR body's line matches the claim's (5952611327, Clause-②: yes): declaring the key widens the published accept set on both faces and opens the strict face to it. This record is the at-tier contract review that clause owes on the landing head.

③ Boundary flags

open_questions: none declared (empty list in 5953952211); none found.

Dev deviations, each answered:

  • Node 22.23.3 downloaded into the issue scratchpad because the container's node sat below jsdom's engines floor. Local toolchain only; nothing of it is in the diff; CI ran on its own 22.x. No contract bearing.
  • Gate-line form (vitest from the root with repo-relative paths; pnpm --filter run type-check). Procedural. No bearing.
  • Commit trailers in AGENTS.md's model-free pair. Procedural, outside this record's inputs. No bearing on the contract.
  • PR body through the relay's actions file, read back identical. No bearing.

Declared narrowing: check:sdui-registration-pins not run locally (needs a console build; the diff adds or removes no registration and touches no sideEffects array). ANSWERED: the head's Bundle Analysis check-run, which carries the performance-budget workflow the dev named, completed success.

Out-of-scope finding (noted, not filed): InputOTPSchema declares defaultValue and error, which the renderer reads by neither name. Pre-existing on BASE, outside the claim's surface, and already carried by the report-only census objectui#4631, which prints it on every run; recorded in the PR's Acceptance notes. ESCALATED to that census's owner by this note; not a condition of this PR.

Check-runs on da4d38ef at 2026-10-02T14:03:51Z: 42 total; 32 success, 3 skipped (the two coverage jobs and dependabot), 0 failure, 7 in_progress: Test shards 1, 2, 3, 4, 7, 8 and Spec Main Shape Gate. Green and bearing on this contract: Type Check, Lint, Build & E2E, Test (dist pins), Test shards 5 and 6, Bundle Analysis, Changeset Declaration, Changeset Bump Policy, Changeset Fixed Group Check, Changeset Claim Re-read, Doc Snippet Type Check, Doc Component Type Check, Governed Surface Queue Guard. Pending, judged: the six test shards hold the vitest population the two new pins belong to; they bear on the PINS' CI evidence, not on the declaration's shape, the semver level or the surface judged above, and the dev reports both pin files green at da4d38ef (15 tests). A red shard would re-open this record. The Spec Main Shape Gate type-checks objectui against a source-built @objectstack/spec from objectstack main; no spec row names input-otp (the dev's measurement of 17.5.0 and of objectstack 1d0600bf), so its result tracks upstream drift, not this diff. Nothing read was red.

Also noted: the PR is behind main and mergeable; the claim's serial constraints are file-disjoint siblings, and the merge-group gates re-run on the merge head.

Implemented-by: claude/issue-11365-input-otp-separator
Reviewed-by: session_01XvhGmGAP79ZB8swnkapxPC

VERDICT: PASS

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants