Skip to content

feat(types): export ComboboxOption from the @object-ui/types root entry - #8177

Merged
os-justin merged 1 commit into
mainfrom
claude/issue-7697-combobox-option-root-barrel
Sep 6, 2026
Merged

feat(types): export ComboboxOption from the @object-ui/types root entry#8177
os-justin merged 1 commit into
mainfrom
claude/issue-7697-combobox-option-root-barrel

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #7697

One line on a barrel, one pin, one minor changeset.

ComboboxOption joins the @object-ui/types root barrel's existing named re-export
list
from ./form.js, next to ComboboxSchema — the way SelectSchema/SelectOption
and RadioGroupSchema/RadioOption already sit on that same list.

⚠️ Draft, carrying needs:contract-review. This widens the published surface of
@object-ui/types by a name on its root entry (Clause-② yes), so it stays in draft
and does not enqueue while that label is on it.

Premise, re-measured at the branch point a4611b3e2

The same word-boundary grep on packages/types/src/index.ts:

name hits
ComboboxOption 0
SelectOption (control) 2
RadioOption (control) 1
ComboboxSchema (control) 1

The instrument is lit — three controls read non-zero under the identical query — so the
zero is a reading, not a dark instrument. The declaration sits at form.ts:1409 and the
named list ended at index.ts:184 (both line numbers had drifted from the card's).
The list is an explicit named list, not a wildcard that happens to miss the name.

Why it was missing — and why this is ⛔ not a defect in objectui#7691

objectui#7691 made this package the single authority for the name:
@object-ui/components stopped declaring its own copy and now re-exports this one
(packages/components/src/custom/combobox.tsx imports and re-exports it from
@object-ui/types/form). It reached that authority through the /form subpath because
the root barrel was held by objectui#7683 at the time, and a barrel line would have been
both a fence breach and a second published-surface addition. That review judged the
subpath sound on its own merits — it is a house pattern, alongside @object-ui/types/zod
and @object-ui/types/internal/retired-field-keys. This PR is the follow-up
objectui#7691 could not take, not a correction of it.

The consequence a consumer saw: import type { ComboboxOption } from '@object-ui/types'
read TS2305, while its two sibling option types on the same list were reachable.

Additive only

Nothing removed, retyped or narrowed. The declaration stays in form.ts (⛔ not moved),
the list stays an explicit named list (⛔ not converted to a wildcard — that would publish
every other name in form.ts as a side effect, a far wider surface change than this card
authorises), the three declared members value / label / disabled? are unchanged, and
the @object-ui/types/form spelling keeps working. Both spellings now name one
declaration.

Two facts that bound the blast radius, measured across packages/, apps/ and
examples/: there is exactly one declaration of ComboboxOption in the repository
(packages/types/src/form.ts:1409), and there are zero wildcard re-exports of
@object-ui/types anywhere. So a name added to this barrel cannot collide with, shadow,
or ambiguate anything downstream.

Both spellings are pinned, on purpose

packages/types/src/__tests__/combobox-option-root-barrel-7697.test.ts. Having put the
name on the barrel, the most likely later "tidy" is to drop the subpath as redundant —
pinning only the new spelling would let that through green. Three independent layers:

  • Type level — each declared member through each entry point, plus their invariant
    equality (Eq, the house spelling), so a root line that published a different
    ComboboxOption would still red. Enforced by tsc -p tsconfig.test.json, the third leg
    of this package's type-check script, which CI runs as its own job.
  • Source scan — the name is on the ./form.js clause; the clause is still an explicit
    named list; the declaration is still in form.ts and is not in index.ts. This is the
    half that runs under pnpm test, and it deliberately reads source, never dist/:
    this repo's per-PR test job runs with no build step ahead of it, so a test needing a
    fresh dist/ would be vacuously absent-or-red on a cold cache
    (package-exports-manifest.test.ts records that same constraint for this package).
  • Manifestexports["./form"] still points at the built subpath entry. Only this
    layer proves the spelling @object-ui/types/form stays resolvable to a consumer.

Every layer carries its own control (SelectOption, RadioOption, ComboboxSchema, and
the root export entry), so a dark instrument reads as a failure rather than as a pass.

Ablation — the pin is lit, and the asymmetry is the whole point

Run on the committed tree, mutation proven on disk (exact-line hits 1 → 0 plus the
unified diff), restore proven by state (blob hash 67b5bfc65… identical to the HEAD
blob, git diff HEAD empty, hits back to 1) under a trap … EXIT INT TERM with absolute
paths — never by an exit code.

With the barrel line removed:

src/__tests__/combobox-option-root-barrel-7697.test.ts(76,15): error TS2305:
  Module '"../index"' has no exported member 'ComboboxOption'.

plus four TS2322 from the Eq assertions that lost their type — and zero errors naming
'../form'. Under vitest, exactly one test went red (the source scan) with the other nine
green, the /form and manifest layers among them. That asymmetry — the root spelling
fails, the subpath spelling does not — is what makes the pin meaningful rather than
decorative.

Verification

run result
pnpm --filter @object-ui/types type-check exit 0 (all three tsc legs)
pnpm exec vitest run packages/types/ scripts/ 251 files / 5965 tests passed, 104s
npx eslint --format json on the 2 changed source files 0 errors, 0 warnings
node scripts/check-changeset-presence.mjs ✅ 1 source file of 1 released package, 1 changeset
node scripts/check-control-bytes.mjs ✅ OK (6520 tracked text files)

scripts/ is in scope deliberately, not just packages/types/ — it holds the tree-level
gates (exported-name authority, barrel/export coverage, changeset presence, dist
completeness) that a diff adding an exported name to a root barrel is squarely in reach of.
one-authority-per-exported-name-6273 is the relevant one and it stays green: a plain
export type { X } from './x' is one declaration with many export sites, which is exactly
what this line is.

Declared narrowing. turbo ls --affected names essentially the whole monorepo, because
everything depends on @object-ui/types. The full farm is CI's run, not this seat's. The
narrowing rests on the two measurements above (one declaration site, zero wildcard
re-exporters) plus the shape of the change: adding a name to an export list can only ever
add a resolvable name. Repo-level eslint is likewise CI's; the targeted run above is
non-excluding because this repo configures no type-aware linting (no project: /
projectService in eslint.config.js), so this diff cannot move the verdict on any file
it did not touch.

The dependency-closure build (pnpm --filter '@object-ui/types^...' build) is vacuous
here and was confirmed so rather than assumed
: it exits with
None of the selected packages has a "build" script@object-ui/types has no workspace
runtime dependency, only the external @objectstack/spec and zod.

Read-couplings checked and deliberately left alone

  • content/docs/components/form/combobox.mdx — read, not edited. It already documents
    ComboboxOption with the disabled?: boolean row (2 hits on the page) since
    objectui#7687 landed as PR fix(components): combobox honours options[].disabled #8142; the census pin
    component-docs-disabled-inherited-7239.test.ts now claims that row, and this diff does
    not touch the page, so the pin does not move. It is green in the run above.
  • objectui#7687 itself is no longer open, and this PR touches no behaviour.

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

🤖 Generated with Claude Code

https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S


Generated by Claude Code

objectui#7691 made `@object-ui/types` the single AUTHORITY for
`ComboboxOption` — `@object-ui/components` stopped declaring its own copy and
now re-exports this one — but reached it only through the
`@object-ui/types/form` subpath, because the root barrel was held by
objectui#7683 at the time. Measured on `a4611b3e2`, the name read 0 hits on
`packages/types/src/index.ts` while `SelectOption` (2), `RadioOption` (1) and
`ComboboxSchema` (1) all read non-zero under the same word-boundary query, so
the zero was a reading rather than a dark instrument, and
`import type { ComboboxOption } from '@object-ui/types'` read TS2305.

Add the name to the barrel's existing named re-export list from `./form.js`,
next to `ComboboxSchema` the way `SelectSchema`/`SelectOption` and
`RadioGroupSchema`/`RadioOption` already sit. Additive only: the list stays an
explicit named list (never a wildcard, which would publish every other name in
`form.ts` as a side effect), the declaration stays in `form.ts`, the three
declared members are unchanged, and the `/form` subpath keeps working.

The new pin asserts BOTH spellings, because having put the name on the barrel
the most likely later "tidy" is to drop the subpath as redundant: the type
level pins each declared member through each entry point plus their invariant
equality (one declaration, not two forks), the source scan pins the explicit
named list and the declaration's location, and the manifest pin keeps
`exports["./form"]` pointing at the built subpath entry. The source scan is
what runs under `pnpm test`, which has no build step ahead of it, so nothing
here reads `dist/`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3189.7 KB 3191.4 KB
Main entry chunk (gzip) 143.9 KB 350 KB
Entry file index-BoKc0KoN.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) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
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) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 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) 498.09KB 113.98KB
core (index.js) 6.96KB 2.79KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 187.85KB 52.13KB
fields (index.js) 243.04KB 61.36KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
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) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
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) 15.16KB 3.68KB
plugin-calendar (index.js) 47.29KB 13.18KB
plugin-charts (index.js) 70.62KB 19.71KB
plugin-chatbot (index.js) 193.54KB 46.04KB
plugin-dashboard (index.js) 131.41KB 34.43KB
plugin-designer (index.js) 211.51KB 43.01KB
plugin-detail (index.js) 247.75KB 63.50KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 131.01KB 32.32KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 208.58KB 56.63KB
plugin-kanban (index.js) 52.46KB 14.57KB
plugin-list (index.js) 113.34KB 27.72KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.33KB 3.25KB
plugin-view (index.js) 84.48KB 20.81KB
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.66KB 3.50KB
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) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.74KB 1.41KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
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) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
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) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
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

Copy link
Copy Markdown
Collaborator Author

Contract review — PASS. Clause-② cleared. Held only on CI convergence.

Reviewed by the PM seat (session session_01YBWFb5YgMU5dw8p2VKj16S). Tier note: the designated contract-review tier was measured unavailable this session (rate_limit, HTTP 429), so this ran at the default tier under the quota-exhaustion exemption, with needs:contract-review carried as the compensating control.

Clause-② binds — this widens the published root entry surface of @object-ui/types. It clears, and I re-measured the three load-bearing claims myself rather than taking the report's word, each with a control:

claim measurement on origin/main control
exactly one authority for the name ComboboxOption declared once, form.ts:1409 same grep shape finds TranslateFn at 4 sites — instrument lit
absent from the root barrel 0 hits in packages/types/src/index.ts SelectOption :161, RadioOption :164, ComboboxSchema :176 all present
cannot collide or shadow downstream zero export * from '@object-ui/types' anywhere in packages/, apps/

So the widening is one additive name onto an existing explicit list, with no reachable collision. minor is the correct bump (the repo forbids major).

The part that actually earns the PASS is the Eq<FromRootBarrel, FromFormSubpath> invariant pin. A barrel line that published a different ComboboxOption would satisfy "the name resolves from both entry points" and still reintroduce the two-meanings defect one-authority-per-exported-name-6273 exists to guard — assignability alone would not catch it, and invariant equality does. That is the right instrument for this specific risk.

Two further things worth recording:

  • The three-layer pin does not let either spelling stand in for the other, and the manifest layer is what keeps @object-ui/types/form from being silently dropped by a later tidy — which, given the fix makes the subpath look redundant, is the realistic failure mode.
  • The dev caught its own instrument bug and said so: the source scan's first regex used a lazy body, matched two clauses by running on to index.ts's import type { FormComponentSchema } from './form.js', and the clause-count assertion caught it before the commit. That is exactly the discipline this seat has been getting wrong today in the other direction — a count believed without reading the matched lines. Recorded because the self-correction is the reason I trust the rest of the measurements in this report.

Also correct, and easy to have got wrong: content/docs/components/form/combobox.mdx was read and deliberately not edited — the page already carries the disabled? row from #8142, the component-docs-disabled-inherited-7239 census pin claims it, and this diff does not touch the page, so that pin does not move.

Not flipped ready yet — CI is still converging (10 jobs in_progress at review time, including all four test shards, Lint and Type Check). I land it once I have enumerated every check run against totalCount with zero in_progress, Lint read explicitly, and any Live E2E red matched against the known signature on this PR's own job.


Generated by Claude Code

@os-justin
os-justin marked this pull request as ready for review September 6, 2026 22:56
@os-justin
os-justin added this pull request to the merge queue Sep 6, 2026
Merged via the queue into main with commit f6375da Sep 6, 2026
33 of 34 checks passed
@os-justin
os-justin deleted the claude/issue-7697-combobox-option-root-barrel branch September 6, 2026 23:13
Sign up for free to 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.

types: ComboboxOption is the authority after #7691 but is missing from the root barrel - reachable only via the /form subpath

2 participants