Skip to content

fix(types): retire FloatingChatbotConfig.triggerIcon as an ADR-0049 tombstone (objectui#7654) - #7669

Draft
os-sam wants to merge 1 commit into
mainfrom
claude/issue-7654-trigger-icon-retire
Draft

fix(types): retire FloatingChatbotConfig.triggerIcon as an ADR-0049 tombstone (objectui#7654)#7669
os-sam wants to merge 1 commit into
mainfrom
claude/issue-7654-trigger-icon-retire

Conversation

@os-sam

@os-sam os-sam commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Part of #7654

Retires FloatingChatbotConfig.triggerIcon under ADR-0049 enforce-or-remove. This is the
triggerIcon half only of the card, per the domain:ui PM split ruling in
comment 5542748081.

Part of, not Fixes, on purpose: the card's other key, displayMode, is a maintainer
decision and the card carries needs-user-decision for it. A closing keyword here would
silently close a card that still has an open decision on it when this merges. #7654 remains
open.

Premise re-measured on this branch's base (f7cf7e8a), with a lit control

The card body's assertions were verified rather than trusted. All three hold:

claim reading
nothing reads triggerIcon git grep over tracked files, build output excluded: 2 hits — the declaration and one historical CHANGELOG line
absent from the registration's inputs confirmed — inputs carries floatingConfig.{position,defaultOpen,panelWidth,panelHeight,title,triggerSize}, not this key
nothing seeds it into defaultProps confirmed — the floatingConfig seed carries the same six keys

Lit control: the same query shape over triggerSize, a key that is read, returns 10
hits across 4 files
. The zeros above are readings, not a dark instrument. Line numbers
drifted from the card (declaration is at complex.ts:928, not :907); the facts did not.

FloatingChatbot destructures six of the interface's seven keys and never this one, and
FloatingChatbotTrigger takes no icon prop at all — so the advertised 'MessageCircle'
default never rendered either.

The finding that changed the shape of the fix: there is no Zod mirror

The brief asked me to check whether FloatingChatbotConfig's mirror is strict or
non-strict. It is neither — there is no mirror at all. floatingConfig sits in the
UnmirroredDeclared ledger (zod-mirror-parity.test.ts, complex.zod.ts#ChatbotSchema),
and BaseSchema ends .passthrough(), so the whole object rides through unvalidated.

Consequences, stated plainly:

A tripwire is pinned instead: the new test asserts that such a node still parses green,
so if #6152 ever mints a FloatingChatbotConfigSchema this goes red — the signal that
whoever lands the mirror must add the retirementTombstone() half at the same time and flip
the control rather than delete it into a vacuum.

Why a tombstone and not a deletion, when the usual argument is unavailable

This repo refuses deletion because an undeclared key is silently stripped by a
non-strict z.object. That argument needs a mirror. With none here, the route was measured
on the tsc channel alone — predicted in writing first, then observed:

route fresh object literal widened (non-fresh) value
deleted TS2353 excess-property error compiles CLEAN
tombstoned TS2322 TS2322

Excess-property checking only reaches a fresh literal, so deletion would have left
const raw = { triggerIcon: 'Sparkles' }; const cfg: FloatingChatbotConfig = raw; silently
accepting a key nothing reads. The declared never makes the assignment itself ill-typed,
so freshness stops mattering. The tombstone strictly dominates deletion on the only
channel available.

Both rows are pinned in the new test — the "deleted" row as a live control on a genuinely
undeclared key, not as prose, so the contrast cannot rot.

Evidence

Instrument named: tsc -p packages/types/tsconfig.test.json --noEmit. Type-level assertions
are erased at runtime, so a green vitest run is not evidence about them. Program membership
proved with --listFiles: complex.ts, complex.zod.ts and the new test file are all
program inputs.

  • Baseline (pre-change)triggerIcon: 'Sparkles' and triggerSize: 56 both compiled
    clean; a bogus key fired TS2353. That third reading is the discrimination control: it
    proves "no error" meant accepted, not not in the program.
  • Deletion ablation — mutation proved on disk (anchored grep 1 to 0; blob b0d9db27 to
    14fdcb53), then restored under an EXIT INT TERM trap using absolute paths, verified by
    empty git diff HEAD and blob-hash equality back to b0d9db27.
  • After the tombstone — both paths error; the six live keys stay writable.
  • Cross-package reverse verificationplugin-chatbot resolves @object-ui/types
    through dist/*.d.ts, so this proves consumers read the rebuilt declaration. Injecting a
    triggerIcon write there turned a green type-check red with exactly one error at the
    injected site; restored and re-verified by blob hash.
  • One prediction MISS, reported as a miss: I predicted the error would read "not
    assignable to type 'never'"
    . It reads 'undefined'?: never under
    exactOptionalPropertyTypes: false is never | undefined, which collapses to undefined.
    The direction was right, the printed type name was wrong. No assertion in this PR depends
    on the word "never".
  • First attempt at the cross-package leg was PRECONDITION NOT MET and is reported as
    NOT MEASURED, never as a pass or a red: the baseline was already red (Cannot find module '@object-ui/components') because the dependency closure had not been built. Closure built,
    leg re-run from a green baseline.

Union re-run after the final commit, at a1170f28:

  • pnpm --filter @object-ui/types type-check — exit 0 (all three tsc projects; script name
    echoed, so not a zero-match)
  • vitest run packages/types/src/__tests__104 files / 1738 tests passed, the
    zod-mirror-parity ledger included
  • pnpm --filter @object-ui/plugin-chatbot type-check — exit 0
  • pnpm lint (whole repo, 47/47 tasks) — exit 0, 0 errors
  • check:control-bytes, check:published-tsconfig-exclude, check:published-dist,
    check:shell-escape-residue, check:doc-fences — all exit 0

Exit codes were captured by redirect before any pipe.

Clause-② determination: yesneeds:contract-review

My own determination, and it survives the mirror finding. FloatingChatbotConfig is
exported from @object-ui/types, so this is a published authoring surface: a write that
compiled for an external consumer now fails their build. That is a contract change and a
breaking one for anyone authoring the key today, even though the in-repo census found no
such site.

What sharpens rather than softens it: the refusal lands in exactly one channel. A
reviewer should not read "tombstone" here and assume the usual two-channel shape — the
runtime accept set is untouched. Labelled on both carriers (#7654 and this PR). Opened as a
draft; not flipped ready, not enqueued.

Scope

packages/types/src/complex.ts and one new test, plus the changeset. displayMode is
untouched
— not tombstoned, not removed, not made live. content/docs/plugins/plugin-chatbot.mdx
is not modified: triggerIcon was never documented there (the Properties section #7656
landed documents the six live floatingConfig keys and correctly omits this one), so the
tombstone required no doc change. No other fenced file was touched.


Generated by Claude Code

…ombstone (objectui#7654)

`triggerIcon` was declared `?: string` with `@default 'MessageCircle'` and read by
nothing. `FloatingChatbot` destructures six of the interface's seven keys and never
this one; `FloatingChatbotTrigger` takes no icon prop, so the advertised default
never rendered. It is absent from the `chatbot-floating` registration's `inputs` and
`defaultProps`, so no designer control offered it and no designer-created node
carries it -- TypeScript was the only way to reach it.

`triggerIcon?: string` becomes `triggerIcon?: never`. The refusal is TYPE-LEVEL
ONLY: `FloatingChatbotConfig` has no Zod mirror, `floatingConfig` sits in the
UnmirroredDeclared ledger, and `BaseSchema` is `.passthrough()`, so runtime parse
behaviour is unchanged. Minting a mirror to host a `retirementTombstone()` would be
the declared-but-unmirrored axis (objectui#6152), a different defect.

Deletion was measured and refused: excess-property checking only reaches a fresh
literal, so a deleted key still rides a widened value silently. The declared `never`
refuses both paths. Both rows are pinned, the "deleted" row as a live control.

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

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3180.1 KB 3191.4 KB
Main entry chunk (gzip) 143.2 KB 350 KB
Entry file index-CI1E9WNz.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) 510.70KB 116.21KB
core (index.js) 6.70KB 2.68KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 180.00KB 50.20KB
fields (index.js) 242.27KB 61.22KB
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) 4.28KB 1.75KB
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.98KB 10.98KB
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.32KB 1.64KB
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.75KB 3.80KB
plugin-calendar (index.js) 48.05KB 13.34KB
plugin-charts (index.js) 70.92KB 19.75KB
plugin-chatbot (index.js) 196.19KB 46.43KB
plugin-dashboard (index.js) 132.86KB 34.68KB
plugin-designer (index.js) 212.86KB 43.19KB
plugin-detail (index.js) 250.55KB 64.06KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 132.87KB 32.66KB
plugin-gantt (index.js) 167.41KB 41.04KB
plugin-grid (index.js) 209.40KB 56.80KB
plugin-kanban (index.js) 52.71KB 14.55KB
plugin-list (index.js) 113.28KB 27.59KB
plugin-map (index.js) 20.57KB 6.82KB
plugin-markdown (index.js) 13.70KB 4.68KB
plugin-report (index.js) 43.57KB 11.96KB
plugin-timeline (index.js) 30.84KB 8.85KB
plugin-tree (index.js) 9.35KB 3.23KB
plugin-view (index.js) 85.24KB 20.94KB
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) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
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) 10.35KB 3.60KB
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 (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.58KB 2.23KB
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

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.

2 participants