Skip to content

refactor(plugin-chatbot): one name, one ChatMessage contract (#4383) - #4400

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-4383-chatmessage-collision
Aug 12, 2026
Merged

refactor(plugin-chatbot): one name, one ChatMessage contract (#4383)#4400
yinlianghui merged 2 commits into
mainfrom
claude/issue-4383-chatmessage-collision

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #4383

@object-ui/plugin-chatbot exported two different ChatMessage types: a minimal one the barrel declared itself (id / role / content / timestamp / avatar / avatarFallback) and the shape ChatbotEnhanced actually renders, re-exported from the same module under the alias ChatbotEnhancedMessage. The natural name resolved to the narrow one, so an importer reaching for ChatMessage silently got the wrong contract — and the compiler could not object, because both shapes existed on purpose and every construction site spreads the extra keys conditionally (...(x ? { toolInvocations } : {})), which defeats excess-property checking. That is how app-shell's AiChatPage ended up unable to read toolInvocations off its own function's return value (#4040; re-pointed in PR #4379, which deliberately left the collision itself standing).

Survey

Command: git grep -nE '\b(ChatMessage|ChatbotEnhancedMessage)\b' -- packages apps examples, dist excluded, then filtered for type references (most raw hits are function names that merely contain the string — uiMessageToChatMessage, hydratedMessagesToChatMessages, sanitizeChatMessagesForCache — plus the unrelated ChatMessageProps / ChatMessageSource / ChatMessageSchema).

Contract Declared in Who reads it Count
legacy minimal ChatMessage plugin-chatbot/src/index.tsx (the barrel) ChatbotProps.messages, ChatMessageProps.messagesame file only 0 importers, in-package or cross-package
enhanced ChatMessage plugin-chatbot/src/ChatbotEnhanced.tsx in-package: mapMessages.ts, useHitlInChat.ts, 3 test suites (all via ../ChatbotEnhanced) 5 modules
↳ same type, via the barrel alias ChatbotEnhancedMessage re-export in index.tsx cross-package: packages/app-shell/src/console/ai/AiChatPage.tsx:88 only 1 importer
(different package, different layer) ChatMessage @object-ui/types complex.ts plugin-chatbot/renderer.tsx, useObjectChat.ts (aliased OuiChatMessage) untouched here — see "third name" below

The decisive number is the first row: nothing imported the legacy shape. It was declared, exported under the most attractive name in the package, and read by no one but the file that declared it.

Shape diff — the enhanced type is a strict superset: every legacy field is present with the identical type, and it adds only optional keys (streaming, toolInvocations, reasoning, sources, traceId, buildProgress, blueprintProgress, charts).

Convergence

Enhanced shape under the natural name; legacy declaration deleted, not renamed:

Pin, and its pre-fix red

packages/plugin-chatbot/src/__tests__/chat-message-contract.test.ts (#4118 convention, matching this package's existing spec-symbol-batch6.test.ts). Compile-time half — a violation is a tsc error under tsconfig.test.json, which is the only lane that can see it, since vitest erases the assertions:

type _BarrelIsEnhanced   = Assert< Equal< BarrelChatMessage, EnhancedChatMessage > >;
type _AliasIsBarrel      = Assert< Equal< ChatbotEnhancedMessage, BarrelChatMessage > >;
type _NotTheRetiredShape = Assert< Equal< Equal< BarrelChatMessage, RetiredMinimalChatMessage >, false > >;

plus probe hygiene (IsAny / IsUnknown), HasIndexSignature false (the objectstack#4075 mechanism — with an index signature every comparison above answers "identical"), one named pin per enhanced-only key, and a widening pin (RetiredMinimalChatMessage extends BarrelChatMessage) that fails the day one of the added keys becomes required. Runtime half: reads index.tsx and fails if the barrel ever declares a message shape of its own again — the one edit tsc reports only in the type-check job.

Reverse verification — direction predicted red, and red is what happened. git checkout HEAD~1 -- packages/plugin-chatbot/src/index.tsx (commit-then-revert; no git stash, per AGENTS.md) restored the legacy barrel declaration:

src/__tests__/chat-message-contract.test.ts(80,37): error TS2344: Type 'false' does not satisfy the constraint 'true'.
src/__tests__/chat-message-contract.test.ts(85,34): error TS2344: Type 'false' does not satisfy the constraint 'true'.
src/__tests__/chat-message-contract.test.ts(91,7):  error TS2344: Type 'false' does not satisfy the constraint 'true'.
...  (11 errors: _BarrelIsEnhanced, _AliasIsBarrel, _NotTheRetiredShape, and all 8 key pins)

and the runtime half: Tests 3 failed | 1 passed (4), with declares no local ChatMessage type reporting packages/plugin-chatbot/src/index.tsx declares a ChatMessage of its own again. Line 84 (_AliasIsEnhanced) stayed green under the reversal, correctly — the alias pointed at the enhanced type in the old barrel too; it is _AliasIsBarrel that carries the collapse. Restored with git checkout HEAD -- …, tree clean.

Verification

Check Result
build closure pnpm --filter '@object-ui/plugin-chatbot^...' build exit 0
repo-root pnpm exec vitest run packages/plugin-chatbot/ Test Files 18 passed (18) · Tests 286 passed (286)
tsc --noEmit (src) + tsc -p tsconfig.test.json (tests) both exit 0
pin is in the program (not vacuous) tsc --listFiles shows chat-message-contract.test.ts and src/index.tsx
repo-wide turbo run type-check --concurrency=2 Tasks: 78 successful, 78 total · exit 0
downstream consumers (pnpm --filter '...@object-ui/plugin-chatbot', prefix = dependents) app-shell, console, site, example-console-starter, example-byo-backend-console — all covered by the repo-wide run, all green
pnpm --filter @object-ui/plugin-chatbot lint 0 errors, 129 warnings, all pre-existing (the new test file's _-prefixed type aliases draw the same no-unused-vars warnings as spec-symbol-batch6.test.ts)
node scripts/check-control-bytes.mjs OK, 4098 files
changeset gates (presence / fixed / no-major) all exit 0

All heavy steps ran under flock /tmp/os-heavy-verify.lock with --max-old-space-size=4096.

Changeset

minor.changeset/chatmessage-one-contract-4383.md, one entry, @object-ui/plugin-chatbot only. The published export surface changes (ChatMessage now denotes a different interface; ChatbotEnhancedMessage becomes deprecated), which per the ruling on #4383 is minor with the breaking semantics spelled out in the body. Never major — AGENTS.md §版本号策略 forbids it outside an @objectstack major sync, and scripts/check-changeset-no-major.mjs enforces it. In practice the change is a widening, so existing values and call sites keep compiling; the body names the case that does not (code relying on the name meaning exactly the six-key shape).

Scope notes


Generated by Claude Code

claude added 2 commits August 12, 2026 01:43
The barrel exported two different `ChatMessage` types: a minimal one it
declared itself (id/role/content/timestamp/avatar/avatarFallback) and the
shape `<ChatbotEnhanced>` actually renders, re-exported under the alias
`ChatbotEnhancedMessage`. The natural name resolved to the narrow one, so
an importer reaching for `ChatMessage` silently got the wrong contract and
the compiler could not object (objectui#4040 / PR #4379).

The minimal declaration is retired; `ChatMessage` now IS the enhanced
shape, and `ChatbotEnhancedMessage` survives as a deprecated alias OF THE
SAME TYPE so existing importers keep compiling. Pinned at compile time in
`__tests__/chat-message-contract.test.ts`, with a runtime net that fails
if the barrel ever declares a message shape of its own again.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@vercel

vercel Bot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 12, 2026 2:10am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation plugin tests labels Aug 12, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 24.7 KB 350 KB
Entry file index-Np3awd3y.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 9.56KB 3.59KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 8.92KB 3.41KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.35KB 1.07KB
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) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 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.65KB 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) 489.20KB 108.43KB
core (index.js) 2.99KB 1.14KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 151.72KB 40.42KB
fields (index.js) 228.36KB 56.61KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 3.35KB 1.38KB
i18n (pickLocalized.js) 3.69KB 1.73KB
i18n (provider.js) 23.12KB 7.62KB
i18n (useDisplayLocale.js) 2.33KB 1.20KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 38.98KB 10.85KB
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.74KB
mobile (index.js) 1.50KB 0.62KB
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.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 45.23KB 12.45KB
plugin-charts (index.js) 62.18KB 17.67KB
plugin-chatbot (index.js) 180.33KB 42.79KB
plugin-dashboard (index.js) 121.56KB 31.63KB
plugin-designer (index.js) 210.91KB 42.67KB
plugin-detail (index.js) 239.00KB 59.76KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 114.58KB 27.68KB
plugin-gantt (index.js) 164.14KB 39.98KB
plugin-grid (index.js) 187.99KB 49.92KB
plugin-kanban (index.js) 48.60KB 13.41KB
plugin-list (index.js) 110.12KB 26.74KB
plugin-map (index.js) 18.05KB 5.80KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 42.27KB 11.36KB
plugin-timeline (index.js) 26.21KB 7.52KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.03KB 20.55KB
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.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 23.71KB 7.96KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.23KB 0.66KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 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-retry.js) 4.32KB 2.02KB
types (index.js) 3.05KB 1.52KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
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 (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
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

ACCEPT — PM 复核 (session session_017Qqyix2QcnpUC9XeYVDzx3), closes #4383.

  • Survey quality is what makes the resolution safe: 189 raw hits filtered to the real type graph, the zero-importer measurement on the legacy shape, and — critically — the identification of the THIRD ChatMessage in @object-ui/types as a different-layer authoring type to leave alone (with the docs correctly recognized as documenting that one). That distinction is exactly what a name-collision card exists to establish.
  • Resolution within the ruling: enhanced shape under the natural name, legacy deleted (nothing imported it), and the @deprecated alias kept on genuine compat evidence (AiChatPage:88, held read-only by app-shell: publishing a draft view does not invalidate the adapter's view caches — the console's real create-view flow bypasses ObjectStackAdapter entirely #4373) — one contract, one canonical name, one deprecated spelling, pinned by the Equal assertion so the two names cannot silently diverge again.
  • Changeset minor with the breaking case named (exact-shape consumers: keyof maps, Equal assertions) is the §版本号策略 branch executed correctly; the widening-in-practice analysis is honest about the unmeasured external tail.
  • Reverse verification predicted red and measured red on 11/12 compile pins plus the runtime net, with the 12th's green correctly explained rather than tidied. Repo-wide 78/78 is the no-downstream-red proof the export change required.
  • [finding] plugin-chatbot's SDUI renderer casts messages as any across the @object-ui/types ↔ plugin-chatbot ChatMessage boundary #4399 (the types↔plugin bridge's three as any casts, dormant today) is filed correctly as an observation-class finding — it enters the pool.

Flipping ready + arming auto-merge.


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 12, 2026 02:25
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 12, 2026
Merged via the queue into main with commit 3256b14 Aug 12, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4383-chatmessage-collision branch August 12, 2026 02:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation plugin tests

Projects

None yet

2 participants