diff --git a/.changeset/25762-duration-keys-carry-unit.md b/.changeset/25762-duration-keys-carry-unit.md new file mode 100644 index 0000000000..d30cf6a233 --- /dev/null +++ b/.changeset/25762-duration-keys-carry-unit.md @@ -0,0 +1,31 @@ +--- +'@object-ui/types': minor +'@object-ui/components': minor +'@object-ui/plugin-chatbot': minor +'@object-ui/plugin-report': minor +--- + +Clause-②: yes +Metadata-protocol: added + +Nine duration keys now carry their unit in their name (objectstack#25762). Each one said its unit only in its description, so `timeout: 300` read as either milliseconds or seconds. After objectui merges into objectstack, `@objectstack/spec`'s `check:duration-unit-keys` judges these schemas too, and the maintainer chose to rename the keys rather than exempt them. The renames follow objectstack's own (`…Ms` / `…Seconds`, the unit each description already stated). The value and its unit do not change, only the key name. This is breaking for a document that still writes an old name; it is marked `minor` under this repository's version policy. + +| Node / schema | FROM | TO | Unit | +|---|---|---|---| +| `carousel` (`CarouselSchema`) | `autoPlay` | `autoPlayMs` | milliseconds | +| `chatbot`, `chatbot-enhanced`, `chatbot-floating` | `autoResponseDelay` | `autoResponseDelayMs` | milliseconds | +| `ActionSchema` | `timeout` | `timeoutMs` | milliseconds | +| `ActionSchema.retry` | `delay` | `delayMs` | milliseconds | +| `toast` (`ToastSchema`) | `duration` | `durationMs` | milliseconds | +| `tooltip` (`TooltipSchema`) | `delayDuration` | `delayDurationMs` | milliseconds | +| `hover-card` (`HoverCardSchema`) | `openDelay` | `openDelayMs` | milliseconds | +| `hover-card` (`HoverCardSchema`) | `closeDelay` | `closeDelayMs` | milliseconds | +| `report` (`ReportComponentSchema`) | `refreshInterval` | `refreshIntervalSeconds` | seconds | + +What an author sees with an old name: + +- **zod (`@object-ui/types/zod`).** The old key is still declared, as a tombstone, so it is refused at its own path with code `invalid_type`. The message names the new key: "`timeout` is now `timeoutMs` … the value (milliseconds) is unchanged." Without the tombstone, `.passthrough()` would keep the value and nothing would read it. +- **TypeScript.** The old member is `?: never` with `@deprecated`, so a typed literal that writes it fails to compile. +- **Renderers.** `toast`, `tooltip`, `hover-card`, the three chatbot registrations and `ReportViewer` read the new key. Their registration `inputs` and `defaultProps` use the new name too. The `useObjectChat` hook option `autoResponseDelay` is a programmatic API, not a schema key, and keeps its name. + +objectui has no conversion registry: ADR-0087's D2 table lives in `@objectstack/spec`. The conversion is this table plus the refusal message. To migrate, rename each key as the table shows and keep the value. diff --git a/content/docs/api/schema-reference.md b/content/docs/api/schema-reference.md index deda5fe567..ecc3c5b6c0 100644 --- a/content/docs/api/schema-reference.md +++ b/content/docs/api/schema-reference.md @@ -667,7 +667,7 @@ A powerful action definition supporting API calls, confirmations, dialogs, chain "condition": "${data.items.length > 0}", "retry": { "maxAttempts": 3, - "delay": 1000 + "delayMs": 1000 } } ``` @@ -689,7 +689,7 @@ A powerful action definition supporting API calls, confirmations, dialogs, chain | `successMessage` / `errorMessage` | `string` | Toast messages on success/failure. | | `reload` | `boolean` | Reload data after action completes. | | `redirect` | `string` | URL to navigate to after action. | -| `retry` | `object` | Retry config with `maxAttempts` and `delay`. | +| `retry` | `object` | Retry config with `maxAttempts` and `delayMs` (milliseconds; renamed from `delay`, objectstack#25762). | **Related:** [DetailSchema](#detailschema), [ButtonSchema](#buttonschema) diff --git a/content/docs/components/feedback/toast.mdx b/content/docs/components/feedback/toast.mdx index 4a831d8c56..a79b8e9ce6 100644 --- a/content/docs/components/feedback/toast.mdx +++ b/content/docs/components/feedback/toast.mdx @@ -10,7 +10,7 @@ The Toast component displays brief notifications to users that appear temporaril ## Basic Usage A `toast` node renders its own trigger button. Clicking the button raises the toast the -node declares — the title, description, variant and duration are all read off the node +node declares — the title, description, variant and `durationMs` are all read off the node itself, so the whole demo is one schema node with no handler to wire up. @@ -38,7 +38,7 @@ interface ToastSchema { variant?: 'default' | 'success' | 'warning' | 'error' | 'info'; // Timing - duration?: number; // Auto-dismiss time in ms + durationMs?: number; // Auto-dismiss time in ms } ``` diff --git a/content/docs/components/overlay/hover-card.mdx b/content/docs/components/overlay/hover-card.mdx index 65a736d1ab..de233eba03 100644 --- a/content/docs/components/overlay/hover-card.mdx +++ b/content/docs/components/overlay/hover-card.mdx @@ -20,8 +20,8 @@ interface HoverCardSchema { content: SchemaNode | SchemaNode[]; // Card content side?: 'top' | 'right' | 'bottom' | 'left'; align?: 'start' | 'center' | 'end'; - openDelay?: number; // Delay before opening (ms) - closeDelay?: number; // Delay before closing (ms) + openDelayMs?: number; // Delay before opening (ms) + closeDelayMs?: number; // Delay before closing (ms) className?: string; } ``` diff --git a/content/docs/components/overlay/tooltip.mdx b/content/docs/components/overlay/tooltip.mdx index 1943f6c5c6..505783f9d3 100644 --- a/content/docs/components/overlay/tooltip.mdx +++ b/content/docs/components/overlay/tooltip.mdx @@ -19,7 +19,7 @@ interface TooltipSchema { children?: SchemaNode | SchemaNode[]; // Rich content — the fallback half side?: 'top' | 'right' | 'bottom' | 'left'; align?: 'start' | 'center' | 'end'; - delayDuration?: number; // Delay before showing (ms) + delayDurationMs?: number; // Delay before showing (ms) } ``` diff --git a/content/docs/core/enhanced-actions.mdx b/content/docs/core/enhanced-actions.mdx index e41e2786bc..039ce15183 100644 --- a/content/docs/core/enhanced-actions.mdx +++ b/content/docs/core/enhanced-actions.mdx @@ -64,8 +64,8 @@ const ajaxAction: ActionSchema = { - `method` - HTTP method (GET, POST, PUT, DELETE, PATCH) - `data` - Request body/payload - `headers` - Custom HTTP headers -- `timeout` - Request timeout in milliseconds -- `retry` - Retry configuration +- `timeoutMs` - Request timeout in milliseconds (renamed from `timeout`) +- `retry` - Retry configuration (`maxAttempts`, `delayMs`) ### Confirm Actions @@ -389,11 +389,11 @@ const retryAction: ActionSchema = { api: '/api/submit', method: 'POST', - timeout: 30000, // 30 seconds + timeoutMs: 30000, // 30 seconds retry: { maxAttempts: 3, - delay: 1000 // 1 second between retries + delayMs: 1000 // 1 second between retries } }; ``` @@ -467,10 +467,10 @@ const complexAction: ActionSchema = { }, // Retry - timeout: 60000, + timeoutMs: 60000, retry: { maxAttempts: 3, - delay: 2000 + delayMs: 2000 }, // Post-action behavior diff --git a/content/docs/core/report-schema.mdx b/content/docs/core/report-schema.mdx index 76ffa98a51..5a28e71da1 100644 --- a/content/docs/core/report-schema.mdx +++ b/content/docs/core/report-schema.mdx @@ -399,7 +399,7 @@ const comprehensiveReport: ReportComponentSchema = { showExportButtons: true, showPrintButton: true, showScheduleButton: true, - refreshInterval: 300 // Auto-refresh every 5 minutes + refreshIntervalSeconds: 300 // Auto-refresh every 5 minutes }; ``` diff --git a/content/docs/plugins/plugin-chatbot.mdx b/content/docs/plugins/plugin-chatbot.mdx index b2dc8e7b80..db3e69e386 100644 --- a/content/docs/plugins/plugin-chatbot.mdx +++ b/content/docs/plugins/plugin-chatbot.mdx @@ -98,7 +98,7 @@ const schema: ChatbotSchema = { maxHeight?: string, autoResponse?: boolean, autoResponseText?: string, - autoResponseDelay?: number, + autoResponseDelayMs?: number, onSend?: (content: string, messages: ObjectChatMessage[]) => void, className?: string, // AI / service-ai integration fields @@ -179,7 +179,7 @@ to apply. They are documented in their own table after this one, under | `maxHeight` | string | `'500px'` | **`chatbot` and `chatbot-enhanced` only** (declared on `ChatbotSchema` and `ChatbotEnhancedSchema`; `ChatbotFloatingSchema` does not declare it). Maximum height of the chat message container, as a CSS length. `chatbot-floating` does not read it: its panel is sized by `floatingConfig.panelHeight` (a **number** of pixels, default `520`), and the panel pins its inner chat to `maxHeight: '100%'` so it fills that panel - a `maxHeight` authored on a floating node would be overridden even if it were forwarded. Size a floating chatbot with `floatingConfig.panelHeight` instead | | `autoResponse` | boolean | `false` | Enable auto-response (demo mode, ignored when `api` is set) | | `autoResponseText` | string | - | Text for auto-response | -| `autoResponseDelay` | number | `1000` | Delay before auto-response (ms) | +| `autoResponseDelayMs` | number | `1000` | Delay before auto-response, in milliseconds (renamed from `autoResponseDelay`, which is now refused with a message naming this key) | | `onSend` | function | - | Callback when message is sent | | `className` | string | `''` | Additional Tailwind CSS classes | | `api` | string | - | Backend SSE endpoint (e.g., `/api/v1/ai/chat`). Enables AI streaming mode | @@ -329,7 +329,7 @@ const schema: ChatbotSchema = { messages: [], // seed with your own ChatMessage values autoResponse: true, autoResponseText: 'Thanks!', - autoResponseDelay: 1000, + autoResponseDelayMs: 1000, }; ``` @@ -554,7 +554,7 @@ const salesBot: ChatbotSchema = { userAvatarFallback: 'You', autoResponse: true, autoResponseText: 'Great choice! Let me show you some options...', - autoResponseDelay: 1200, + autoResponseDelayMs: 1200, maxHeight: '500px', className: 'border-2 border-primary rounded-xl' }; diff --git a/examples/schema-catalog/src/schemas/components-feedback-toast/success-toast.json b/examples/schema-catalog/src/schemas/components-feedback-toast/success-toast.json index 9309c6a395..56383f76fb 100644 --- a/examples/schema-catalog/src/schemas/components-feedback-toast/success-toast.json +++ b/examples/schema-catalog/src/schemas/components-feedback-toast/success-toast.json @@ -3,5 +3,5 @@ "variant": "success", "title": "Saved Successfully", "description": "Your changes have been saved.", - "duration": 3000 + "durationMs": 3000 } diff --git a/examples/schema-catalog/src/schemas/plugin-chatbot/basic-chatbot.json b/examples/schema-catalog/src/schemas/plugin-chatbot/basic-chatbot.json index 505b727c49..78df0d4dd7 100644 --- a/examples/schema-catalog/src/schemas/plugin-chatbot/basic-chatbot.json +++ b/examples/schema-catalog/src/schemas/plugin-chatbot/basic-chatbot.json @@ -11,6 +11,6 @@ "showTimestamp": false, "autoResponse": true, "autoResponseText": "Thank you for your message! This is an automated response.", - "autoResponseDelay": 1000, + "autoResponseDelayMs": 1000, "maxHeight": "400px" } diff --git a/examples/schema-catalog/src/schemas/plugin-chatbot/customer-support-chat.json b/examples/schema-catalog/src/schemas/plugin-chatbot/customer-support-chat.json index 29021302c3..e125c9511f 100644 --- a/examples/schema-catalog/src/schemas/plugin-chatbot/customer-support-chat.json +++ b/examples/schema-catalog/src/schemas/plugin-chatbot/customer-support-chat.json @@ -30,7 +30,7 @@ "assistantAvatarFallback": "SA", "showTimestamp": true, "autoResponse": true, - "autoResponseDelay": 1500, + "autoResponseDelayMs": 1500, "autoResponseText": "Let me check that for you...", "maxHeight": "500px" } diff --git a/packages/components/src/renderers/feedback/toast.tsx b/packages/components/src/renderers/feedback/toast.tsx index 0faa7aa3ce..8700271413 100644 --- a/packages/components/src/renderers/feedback/toast.tsx +++ b/packages/components/src/renderers/feedback/toast.tsx @@ -22,7 +22,7 @@ ComponentRegistry.register('toast', toastFn(schema.title || 'Notification', { description: schema.description, - duration: schema.duration, + duration: schema.durationMs, }); }; @@ -42,7 +42,7 @@ ComponentRegistry.register('toast', name: 'variant', type: 'enum', enum: ['default', 'success', 'warning', 'error', 'info'] }, - { name: 'duration', type: 'number' }, + { name: 'durationMs', type: 'number' }, { name: 'buttonLabel', type: 'string' }, { name: 'buttonVariant', @@ -57,7 +57,7 @@ ComponentRegistry.register('toast', title: 'Notification', buttonLabel: 'Show Toast', variant: 'default', - duration: 5000 + durationMs: 5000 } } ); diff --git a/packages/components/src/renderers/overlay/hover-card.tsx b/packages/components/src/renderers/overlay/hover-card.tsx index 0132ba7e2c..10219696fc 100644 --- a/packages/components/src/renderers/overlay/hover-card.tsx +++ b/packages/components/src/renderers/overlay/hover-card.tsx @@ -17,7 +17,7 @@ import { renderChildren, renderTriggerSlot } from '../../lib/utils'; ComponentRegistry.register('hover-card', ({ schema, className, ...props }: { schema: HoverCardSchema; className?: string; [key: string]: any }) => ( - + {renderTriggerSlot(HoverCardTrigger, schema.trigger)} {renderChildren(schema.content)} @@ -28,8 +28,8 @@ ComponentRegistry.register('hover-card', namespace: 'ui', label: 'Hover Card', inputs: [ - { name: 'openDelay', type: 'number' }, - { name: 'closeDelay', type: 'number' }, + { name: 'openDelayMs', type: 'number' }, + { name: 'closeDelayMs', type: 'number' }, { name: 'side', type: 'enum', enum: ['top', 'right', 'bottom', 'left'] }, { name: 'align', type: 'enum', enum: ['start', 'center', 'end'] }, { diff --git a/packages/components/src/renderers/overlay/tooltip.tsx b/packages/components/src/renderers/overlay/tooltip.tsx index 79283a2b9b..1f9cbec8c4 100644 --- a/packages/components/src/renderers/overlay/tooltip.tsx +++ b/packages/components/src/renderers/overlay/tooltip.tsx @@ -22,7 +22,7 @@ import { renderChildren, renderTriggerSlot } from '../../lib/utils'; ComponentRegistry.register('tooltip', ({ schema, className, ...props }: { schema: TooltipSchema; className?: string; [key: string]: any }) => ( - + {renderTriggerSlot(TooltipTrigger, schema.trigger)} @@ -35,7 +35,7 @@ ComponentRegistry.register('tooltip', namespace: 'ui', label: 'Tooltip', inputs: [ - { name: 'delayDuration', type: 'number' }, + { name: 'delayDurationMs', type: 'number' }, { name: 'side', type: 'enum', enum: ['top', 'right', 'bottom', 'left'] }, { name: 'align', type: 'enum', enum: ['start', 'center', 'end'] }, { @@ -58,7 +58,7 @@ ComponentRegistry.register('tooltip', defaultProps: { trigger: [{ type: 'button', label: 'Hover me', variant: 'outline' }], content: 'Tooltip content', - delayDuration: 700, + delayDurationMs: 700, side: 'top' } } diff --git a/packages/plugin-chatbot/src/__tests__/renderer.floating-spread-fence-7708.test.tsx b/packages/plugin-chatbot/src/__tests__/renderer.floating-spread-fence-7708.test.tsx index ad933f5472..a0306a2ee3 100644 --- a/packages/plugin-chatbot/src/__tests__/renderer.floating-spread-fence-7708.test.tsx +++ b/packages/plugin-chatbot/src/__tests__/renderer.floating-spread-fence-7708.test.tsx @@ -108,7 +108,7 @@ describe('chatbot-floating: a sent message renders on the panel (fixed, objectui messages: [{ id: 'seed', role: 'assistant', content: 'seed-message-marker' }], autoResponse: true, autoResponseText: 'auto-reply-marker', - autoResponseDelay: 0, + autoResponseDelayMs: 0, }); // Before the fix: the raw `{...props}` spread, LAST, put the AUTHORED diff --git a/packages/plugin-chatbot/src/renderer.tsx b/packages/plugin-chatbot/src/renderer.tsx index 5707a5d860..3ac8b9bb6b 100644 --- a/packages/plugin-chatbot/src/renderer.tsx +++ b/packages/plugin-chatbot/src/renderer.tsx @@ -29,7 +29,7 @@ import { toRuntimeMessages } from './chatMessageAdapter'; * * **Legacy Mode** (when `api` is not set): * - Local auto-response for demo/playground use - * - Schema fields: autoResponse, autoResponseText, autoResponseDelay + * - Schema fields: autoResponse, autoResponseText, autoResponseDelayMs * * Both modes support the `onSend` callback: * - Signature: `onSend(content: string, messages: ObjectChatMessage[]): void` @@ -93,7 +93,7 @@ ComponentRegistry.register('chatbot', showTimestamp: schema.showTimestamp, autoResponse: schema.autoResponse, autoResponseText: schema.autoResponseText, - autoResponseDelay: schema.autoResponseDelay, + autoResponseDelay: schema.autoResponseDelayMs, onSend: schema.onSend, }); @@ -214,7 +214,7 @@ ComponentRegistry.register('chatbot', name: 'autoResponseText', type: 'string' }, { - name: 'autoResponseDelay', + name: 'autoResponseDelayMs', type: 'number' }, { name: 'className', @@ -236,7 +236,7 @@ ComponentRegistry.register('chatbot', maxHeight: '500px', autoResponse: true, autoResponseText: 'Thank you for your message! This is an automated response.', - autoResponseDelay: 1000, + autoResponseDelayMs: 1000, className: 'w-full max-w-2xl' } } @@ -281,7 +281,7 @@ ComponentRegistry.register('chatbot-enhanced', showTimestamp: schema.showTimestamp, autoResponse: schema.autoResponse, autoResponseText: schema.autoResponseText, - autoResponseDelay: schema.autoResponseDelay, + autoResponseDelay: schema.autoResponseDelayMs, onSend: schema.onSend, }); @@ -352,7 +352,7 @@ ComponentRegistry.register('chatbot-enhanced', { name: 'streamingEnabled', type: 'boolean' }, { name: 'autoResponse', type: 'boolean' }, { name: 'autoResponseText', type: 'string' }, - { name: 'autoResponseDelay', type: 'number' }, + { name: 'autoResponseDelayMs', type: 'number' }, { name: 'className', type: 'string' } ], defaultProps: { @@ -373,7 +373,7 @@ ComponentRegistry.register('chatbot-enhanced', maxHeight: '500px', autoResponse: true, autoResponseText: 'Thank you for your message! This is an automated response.', - autoResponseDelay: 1000, + autoResponseDelayMs: 1000, className: 'w-full max-w-2xl' } } @@ -418,7 +418,7 @@ ComponentRegistry.register('chatbot-floating', showTimestamp: schema.showTimestamp, autoResponse: schema.autoResponse, autoResponseText: schema.autoResponseText, - autoResponseDelay: schema.autoResponseDelay, + autoResponseDelay: schema.autoResponseDelayMs, onSend: schema.onSend, }); @@ -504,7 +504,7 @@ ComponentRegistry.register('chatbot-floating', { name: 'streamingEnabled', type: 'boolean' }, { name: 'autoResponse', type: 'boolean' }, { name: 'autoResponseText', type: 'string' }, - { name: 'autoResponseDelay', type: 'number' }, + { name: 'autoResponseDelayMs', type: 'number' }, { name: 'className', type: 'string' }, ], defaultProps: { @@ -528,7 +528,7 @@ ComponentRegistry.register('chatbot-floating', enableFileUpload: false, autoResponse: true, autoResponseText: 'Thank you for your message! This is an automated response.', - autoResponseDelay: 1000, + autoResponseDelayMs: 1000, } } ); diff --git a/packages/plugin-report/src/ReportViewer.tsx b/packages/plugin-report/src/ReportViewer.tsx index 62f11debf3..7dc60a507b 100644 --- a/packages/plugin-report/src/ReportViewer.tsx +++ b/packages/plugin-report/src/ReportViewer.tsx @@ -241,7 +241,7 @@ export const ReportViewer: React.FC = ({ schema, onRefresh }) rule itself lives in the shared print sheet (`@object-ui/app-shell/styles.css`, objectui#4462). */} - {report.refreshInterval && ( + {report.refreshIntervalSeconds && ( Refresh diff --git a/packages/types/src/__tests__/chatbot-authoring-face-keys.test.ts b/packages/types/src/__tests__/chatbot-authoring-face-keys.test.ts index 03e01d2c4a..ebf57d772c 100644 --- a/packages/types/src/__tests__/chatbot-authoring-face-keys.test.ts +++ b/packages/types/src/__tests__/chatbot-authoring-face-keys.test.ts @@ -13,7 +13,8 @@ * * Before this file, `showTimestamp`, `userAvatarUrl`, `userAvatarFallback`, * `assistantAvatarUrl`, `assistantAvatarFallback`, `maxHeight`, - * `autoResponse`, `autoResponseText`, `autoResponseDelay` and `onSend` lived + * `autoResponse`, `autoResponseText`, `autoResponseDelay` (renamed + * `autoResponseDelayMs` by objectstack#25762) and `onSend` lived * ONLY in an anonymous inline intersection at * `packages/plugin-chatbot/src/renderer.tsx`'s `chatbot` registration site — * nothing outside that one file could reference, validate, or document them. @@ -78,12 +79,12 @@ describe('ChatbotSchema: the ten local-display/legacy keys are declared, not ano maxHeight: '500px', autoResponse: true, autoResponseText: 'Thanks!', - autoResponseDelay: 1000, + autoResponseDelayMs: 1000, onSend, }; expect(node.showTimestamp).toBe(true); - expect(node.autoResponseDelay).toBe(1000); + expect(node.autoResponseDelayMs).toBe(1000); node.onSend?.('hello', baseMessages); }); @@ -91,10 +92,10 @@ describe('ChatbotSchema: the ten local-display/legacy keys are declared, not ano const node: ChatbotSchema = { type: 'chatbot', messages: baseMessages, - // @ts-expect-error `autoResponseDelay` is declared `number`; before this + // @ts-expect-error `autoResponseDelayMs` is declared `number`; before this // field was named, `[key: string]: any` on `BaseSchema` swallowed any // value at any unlisted key and this line type-checked. - autoResponseDelay: 'not-a-number', + autoResponseDelayMs: 'not-a-number', }; // Runtime shape is unaffected by the type-level assertion above. expect(node.type).toBe('chatbot'); @@ -125,7 +126,7 @@ describe('ChatbotSchema: the ten local-display/legacy keys are declared, not ano maxHeight: '500px', autoResponse: true, autoResponseText: 'Thanks!', - autoResponseDelay: 1000, + autoResponseDelayMs: 1000, }); expect(result.success).toBe(true); @@ -152,14 +153,14 @@ describe('ChatbotSchema: the ten local-display/legacy keys are declared, not ano const result = ChatbotZodSchema.safeParse({ type: 'chatbot', messages: [], - autoResponseDelay: 'not-a-number', + autoResponseDelayMs: 'not-a-number', }); expect(result.success).toBe(false); if (!result.success) { expect( result.error.issues.some( - (issue) => issue.path.join('.') === 'autoResponseDelay' && issue.code === 'invalid_type', + (issue) => issue.path.join('.') === 'autoResponseDelayMs' && issue.code === 'invalid_type', ), ).toBe(true); } @@ -176,7 +177,7 @@ describe('ChatbotSchema: the ten local-display/legacy keys are declared, not ano 'maxHeight', 'autoResponse', 'autoResponseText', - 'autoResponseDelay', + 'autoResponseDelayMs', 'onSend', ]) { expect(shapeKeys).toContain(key); diff --git a/packages/types/src/__tests__/chatbot-registration-authoring-faces-7655.test.ts b/packages/types/src/__tests__/chatbot-registration-authoring-faces-7655.test.ts index 3a402455ca..1ce4f937f5 100644 --- a/packages/types/src/__tests__/chatbot-registration-authoring-faces-7655.test.ts +++ b/packages/types/src/__tests__/chatbot-registration-authoring-faces-7655.test.ts @@ -122,7 +122,7 @@ type SharedKeys = | 'messages' | 'placeholder' | 'api' | 'conversationId' | 'systemPrompt' | 'model' | 'streamingEnabled' | 'headers' | 'requestBody' | 'onError' | 'showTimestamp' | 'userAvatarUrl' | 'userAvatarFallback' | 'assistantAvatarUrl' - | 'assistantAvatarFallback' | 'autoResponse' | 'autoResponseText' | 'autoResponseDelay' + | 'assistantAvatarFallback' | 'autoResponse' | 'autoResponseText' | 'autoResponseDelayMs' | 'onSend'; /* ── The discriminants: one node, one type ───────────────────────────────── */ @@ -137,8 +137,11 @@ export type assertionOneDiscriminantPerFace = [ export type assertionSharedKeyAliasIsTheCensus = Expect>; -/** The objectui#5605 tombstone, declared (as `?: never`) on all three faces by one pick. */ -type RetiredOnEveryFace = 'maxToolRoundtrips'; +/** + * The objectui#5605 tombstone and the objectstack#25762 rename tombstone (`autoResponseDelay`, now + * `autoResponseDelayMs` in `SharedKeys`), each declared (as `?: never`) on all three faces by one pick. + */ +type RetiredOnEveryFace = 'maxToolRoundtrips' | 'autoResponseDelay'; /** `chatbot-enhanced`: the shared nineteen + the tombstone + `maxHeight`, `processVisibility`, and its four own keys. */ export type assertionEnhancedDeclaresWhatItReads = Expect< @@ -223,6 +226,13 @@ export type assertionMaxToolRoundtripsIsATombstoneOnEveryFace = [ Expect>, ]; +/** ── `autoResponseDelay`: renamed `autoResponseDelayMs` on all three faces (objectstack#25762) ── */ +export type assertionAutoResponseDelayIsATombstoneOnEveryFace = [ + Expect>, + Expect>, + Expect>, +]; + /* ── `displayMode`: one type on both faces; `floatingConfig`: live on the floating face only ── */ /** @@ -488,7 +498,7 @@ describe('the census is structural: picked off `ChatbotSchema`, never copied', ( 'messages', 'placeholder', 'api', 'conversationId', 'systemPrompt', 'model', 'streamingEnabled', 'headers', 'requestBody', 'onError', 'showTimestamp', 'userAvatarUrl', 'userAvatarFallback', 'assistantAvatarUrl', 'assistantAvatarFallback', 'autoResponse', 'autoResponseText', - 'autoResponseDelay', 'onSend', + 'autoResponseDelayMs', 'onSend', ]; it('every shared arm on the two twins carries `ChatbotSchema`\'s own description — one spelling', () => { diff --git a/packages/types/src/__tests__/duration-unit-key-renames-25762.test.ts b/packages/types/src/__tests__/duration-unit-key-renames-25762.test.ts new file mode 100644 index 0000000000..111ee0b3c6 --- /dev/null +++ b/packages/types/src/__tests__/duration-unit-key-renames-25762.test.ts @@ -0,0 +1,161 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * Rename pin — the nine duration keys whose unit lived only in their describe + * now carry it in their NAME (objectstack#25762, maintainer decision E on the + * objectui → objectstack merge, objectstack#25677). + * + * After the merge, `@objectstack/spec`'s `check:duration-unit-keys` judges these + * mirrors too; it refuses a numeric key whose describe names a unit its key name + * lacks. The renames follow objectstack's own (objectstack#15679 / + * objectstack#15680): `…Ms` / `…Seconds`, the unit the describe already stated. + * The instrument that re-derives the population is that gate, not this file — + * the table below is the nine renames this card made, pinned so none of them + * can silently come back under its old name. + * + * Each old spelling stays DECLARED as a `retirementTombstone` fed by + * `durationKeyRenamedGuidance` (`zod/tombstone.zod.ts`): an authored value is + * refused at the key's own path, by name, with a message that names the new + * key, instead of riding `.passthrough()` unread. The TypeScript twin of each is + * a `?: never` member, so a typed literal refuses it at the authoring site; the + * `@ts-expect-error` lines below are checked by this package's `type-check` + * (`tsconfig.test.json`), and a re-declared old key leaves a directive unused, + * which fails `tsc`. + */ + +import { describe, it, expect } from 'vitest'; +import type { z } from 'zod'; +import type { ActionSchema as ActionInterface } from '../crud'; +import type { CarouselSchema as CarouselInterface, ChatbotSchema as ChatbotInterface } from '../complex'; +import type { ToastSchema as ToastInterface } from '../feedback'; +import type { HoverCardSchema as HoverCardInterface, TooltipSchema as TooltipInterface } from '../overlay'; +import type { ReportComponentSchema as ReportInterface } from '../reports'; +import { + ActionSchema, + CarouselSchema, + ChatbotEnhancedSchema, + ChatbotFloatingSchema, + ChatbotSchema, + HoverCardSchema, + ReportComponentSchema, + ToastSchema, + TooltipSchema, +} from '../zod/index.zod.js'; + +interface RenameCase { + /** The schema the key is a member of. */ + schema: z.ZodType; + /** A document that parses green without either spelling — the lit control. */ + base: Record; + /** Path of the member, from the document root. */ + at: readonly string[]; + retired: string; + renamed: string; + unit: 'milliseconds' | 'seconds'; +} + +const node: Pick = { + content: [{ type: 'text', content: 'x' }], + trigger: [{ type: 'text', content: 'y' }], +}; + +const CASES: readonly RenameCase[] = [ + { schema: CarouselSchema, base: { type: 'carousel', items: [] }, at: [], retired: 'autoPlay', renamed: 'autoPlayMs', unit: 'milliseconds' }, + { schema: ChatbotSchema, base: { type: 'chatbot', messages: [] }, at: [], retired: 'autoResponseDelay', renamed: 'autoResponseDelayMs', unit: 'milliseconds' }, + { schema: ChatbotEnhancedSchema, base: { type: 'chatbot-enhanced', messages: [] }, at: [], retired: 'autoResponseDelay', renamed: 'autoResponseDelayMs', unit: 'milliseconds' }, + { schema: ChatbotFloatingSchema, base: { type: 'chatbot-floating', messages: [] }, at: [], retired: 'autoResponseDelay', renamed: 'autoResponseDelayMs', unit: 'milliseconds' }, + { schema: ActionSchema, base: { type: 'action', label: 'Go' }, at: [], retired: 'timeout', renamed: 'timeoutMs', unit: 'milliseconds' }, + { schema: ActionSchema, base: { type: 'action', label: 'Go', retry: { maxAttempts: 2 } }, at: ['retry'], retired: 'delay', renamed: 'delayMs', unit: 'milliseconds' }, + { schema: ToastSchema, base: { type: 'toast' }, at: [], retired: 'duration', renamed: 'durationMs', unit: 'milliseconds' }, + { schema: TooltipSchema, base: { type: 'tooltip', content: 'Hi' }, at: [], retired: 'delayDuration', renamed: 'delayDurationMs', unit: 'milliseconds' }, + { schema: HoverCardSchema, base: { type: 'hover-card', ...node }, at: [], retired: 'openDelay', renamed: 'openDelayMs', unit: 'milliseconds' }, + { schema: HoverCardSchema, base: { type: 'hover-card', ...node }, at: [], retired: 'closeDelay', renamed: 'closeDelayMs', unit: 'milliseconds' }, + { schema: ReportComponentSchema, base: { type: 'report' }, at: [], retired: 'refreshInterval', renamed: 'refreshIntervalSeconds', unit: 'seconds' }, +]; + +/** `base` with `value` written at `at` + `key`, leaving `base` untouched. */ +function withKey(base: Record, at: readonly string[], key: string, value: unknown): Record { + if (at.length === 0) return { ...base, [key]: value }; + const [head, ...rest] = at; + return { ...base, [head]: withKey((base[head] ?? {}) as Record, rest, key, value) }; +} + +describe('the nine duration keys carry their unit in their name (objectstack#25762)', () => { + it('covers nine distinct renames — the population the gate named on the merged tree', () => { + expect(new Set(CASES.map((c) => `${c.retired}→${c.renamed}`)).size).toBe(9); + }); + + for (const c of CASES) { + const label = `${String((c.base as { type: string }).type)} ${[...c.at, c.retired].join('.')} → ${c.renamed}`; + + it(`${label}: the base document parses, and the renamed key accepts a number`, () => { + expect(c.schema.safeParse(c.base).success).toBe(true); + expect(c.schema.safeParse(withKey(c.base, c.at, c.renamed, 250)).success).toBe(true); + }); + + it(`${label}: the renamed key still refuses a non-number`, () => { + const result = c.schema.safeParse(withKey(c.base, c.at, c.renamed, '250')); + expect(result.success).toBe(false); + expect(result.error?.issues.map((i) => i.path.join('.'))).toEqual([[...c.at, c.renamed].join('.')]); + }); + + it(`${label}: the old spelling is refused by name, at its own path, naming the new key`, () => { + const result = c.schema.safeParse(withKey(c.base, c.at, c.retired, 250)); + expect(result.success).toBe(false); + const issues = result.error?.issues ?? []; + expect(issues.map((i) => [i.path.join('.'), i.code])).toEqual([[[...c.at, c.retired].join('.'), 'invalid_type']]); + expect(issues[0]?.message).toContain(`\`${c.retired}\` is now \`${c.renamed}\``); + expect(issues[0]?.message).toContain(`the value (${c.unit}) is unchanged`); + }); + } +}); + +/* ── TypeScript face ───────────────────────────────────────────────────────── */ + +describe('the TypeScript twins refuse the old spelling at the authoring site (objectstack#25762)', () => { + it('the renamed key compiles on every twin; the old one is a compile error', () => { + const carousel: CarouselInterface = { type: 'carousel', items: [], autoPlayMs: 3000 }; + // @ts-expect-error — `autoPlay` was renamed `autoPlayMs`. + const carouselOld: CarouselInterface = { type: 'carousel', items: [], autoPlay: 3000 }; + + const chatbot: ChatbotInterface = { type: 'chatbot', messages: [], autoResponseDelayMs: 0 }; + // @ts-expect-error — `autoResponseDelay` was renamed `autoResponseDelayMs`. + const chatbotOld: ChatbotInterface = { type: 'chatbot', messages: [], autoResponseDelay: 0 }; + + const action: ActionInterface = { type: 'action', label: 'Go', timeoutMs: 30_000, retry: { delayMs: 1000 } }; + // @ts-expect-error — `timeout` was renamed `timeoutMs`. + const actionOld: ActionInterface = { type: 'action', label: 'Go', timeout: 30_000 }; + // @ts-expect-error — `retry.delay` was renamed `retry.delayMs`. + const retryOld: ActionInterface = { type: 'action', label: 'Go', retry: { delay: 1000 } }; + + const toast: ToastInterface = { type: 'toast', durationMs: 3000 }; + // @ts-expect-error — `duration` was renamed `durationMs`. + const toastOld: ToastInterface = { type: 'toast', duration: 3000 }; + + const tooltip: TooltipInterface = { type: 'tooltip', content: 'Hi', delayDurationMs: 200 }; + // @ts-expect-error — `delayDuration` was renamed `delayDurationMs`. + const tooltipOld: TooltipInterface = { type: 'tooltip', content: 'Hi', delayDuration: 200 }; + + const hoverCard: HoverCardInterface = { type: 'hover-card', ...node, openDelayMs: 200, closeDelayMs: 300 }; + // @ts-expect-error — `openDelay` was renamed `openDelayMs`. + const openOld: HoverCardInterface = { type: 'hover-card', ...node, openDelay: 200 }; + // @ts-expect-error — `closeDelay` was renamed `closeDelayMs`. + const closeOld: HoverCardInterface = { type: 'hover-card', ...node, closeDelay: 300 }; + + const report: ReportInterface = { type: 'report', refreshIntervalSeconds: 300 }; + // @ts-expect-error — `refreshInterval` was renamed `refreshIntervalSeconds`. + const reportOld: ReportInterface = { type: 'report', refreshInterval: 300 }; + + // Runtime shape is unaffected by the type-level assertions above. + expect([carousel, chatbot, action, toast, tooltip, hoverCard, report].map((n) => n.type)).toEqual([ + 'carousel', 'chatbot', 'action', 'toast', 'tooltip', 'hover-card', 'report', + ]); + expect([carouselOld, chatbotOld, actionOld, retryOld, toastOld, tooltipOld, openOld, closeOld, reportOld]).toHaveLength(9); + }); +}); diff --git a/packages/types/src/__tests__/phase2-schemas.test.ts b/packages/types/src/__tests__/phase2-schemas.test.ts index cd7cc69d6b..e632308e3e 100644 --- a/packages/types/src/__tests__/phase2-schemas.test.ts +++ b/packages/types/src/__tests__/phase2-schemas.test.ts @@ -583,10 +583,10 @@ describe('Phase 2: Enhanced ActionSchema Zod Validation', () => { label: 'Submit', actionType: 'ajax', api: '/api/submit', - timeout: 30000, + timeoutMs: 30000, retry: { maxAttempts: 3, - delay: 1000, + delayMs: 1000, }, }; diff --git a/packages/types/src/complex.ts b/packages/types/src/complex.ts index 88bff5e11a..3525e7c759 100644 --- a/packages/types/src/complex.ts +++ b/packages/types/src/complex.ts @@ -863,7 +863,14 @@ export interface CarouselSchema extends BaseSchema { /** * Auto-play interval (ms) */ - autoPlay?: number; + autoPlayMs?: number; + /** + * RENAMED to {@link CarouselSchema.autoPlayMs} (objectstack#25762, ADR-0087): a duration + * carries its unit in its key name, never only in its description. The + * value (milliseconds) is unchanged; the zod twin refuses this spelling by name. + * @deprecated Renamed to `autoPlayMs`. + */ + autoPlay?: never; /** * Show navigation arrows * @default true @@ -1496,7 +1503,14 @@ export interface ChatbotSchema extends BaseSchema { * Delay in milliseconds before the local auto-response is sent. * @default 1000 */ - autoResponseDelay?: number; + autoResponseDelayMs?: number; + /** + * RENAMED to {@link ChatbotSchema.autoResponseDelayMs} (objectstack#25762, ADR-0087): a duration + * carries its unit in its key name, never only in its description. The + * value (milliseconds) is unchanged; the zod twin refuses this spelling by name. + * @deprecated Renamed to `autoResponseDelayMs`. + */ + autoResponseDelay?: never; /** * Called after a message is sent, in both API and local auto-response mode, * with the trimmed content and the full message list at that point. @@ -1695,7 +1709,7 @@ export interface ChatbotSchema extends BaseSchema { * scores. Every read is filed under the TYPE of the object it is read from; * this declaration carries none. What the renderer DOES read off this node: * `api`, `assistantAvatarFallback`, `assistantAvatarUrl`, `autoResponse`, - * `autoResponseDelay`, `autoResponseText`, `conversationId`, `headers`, + * `autoResponseDelayMs`, `autoResponseText`, `conversationId`, `headers`, * `maxHeight`, `messages`, `model`, `onError`, `onSend`, * `placeholder`, `requestBody`, `showTimestamp`, `streamingEnabled`, * `systemPrompt`, `userAvatarFallback`, `userAvatarUrl` (in @@ -1755,7 +1769,7 @@ export type ChatbotSharedKey = | 'assistantAvatarFallback' | 'autoResponse' | 'autoResponseText' - | 'autoResponseDelay' + | 'autoResponseDelayMs' | 'onSend'; /** @@ -1777,6 +1791,8 @@ export type ChatbotSharedKey = * - the nineteen {@link ChatbotSharedKey} members every registration reads; * - the `maxToolRoundtrips` retirement tombstone (objectui#5605), picked off * {@link ChatbotSchema} so all three chat nodes refuse it with one declaration; + * - the `autoResponseDelay` rename tombstone (objectstack#25762, now + * `autoResponseDelayMs`), picked off {@link ChatbotSchema} the same way; * - `maxHeight` and `processVisibility`, which `chatbot-enhanced` forwards to * `` by name and `chatbot-floating` has no named read for * (its panel is sized by `floatingConfig.panelHeight`; for the second, @@ -1794,7 +1810,7 @@ export type ChatbotSharedKey = */ export interface ChatbotEnhancedSchema extends BaseSchema, - Pick { + Pick { type: 'chatbot-enhanced'; /** * Render assistant messages as markdown. Forwarded to ``'s @@ -1847,7 +1863,7 @@ export interface ChatbotEnhancedSchema * scores. Every read is filed under the TYPE of the object it is read from; * this declaration carries none. What the renderer DOES read off this node: * `api`, `assistantAvatarFallback`, `assistantAvatarUrl`, `autoResponse`, - * `autoResponseDelay`, `autoResponseText`, `conversationId`, + * `autoResponseDelayMs`, `autoResponseText`, `conversationId`, * `enableFileUpload`, `enableMarkdown`, `headers`, `maxHeight`, * `messages`, `model`, `onClear`, `onError`, `onSend`, * `placeholder`, `processVisibility`, `requestBody`, `showTimestamp`, @@ -1880,7 +1896,8 @@ export interface ChatbotEnhancedSchema * censused per key on the PR's base: * * - the nineteen {@link ChatbotSharedKey} members; - * - the `maxToolRoundtrips` retirement tombstone (objectui#5605); + * - the `maxToolRoundtrips` retirement tombstone (objectui#5605) and the + * `autoResponseDelay` rename tombstone (objectstack#25762); * - `enableMarkdown`, `enableFileUpload` and the `onClear` runtime slot, * forwarded into the panel's ``; * - `floatingConfig`, the trigger and panel geometry @@ -1911,7 +1928,7 @@ export interface ChatbotEnhancedSchema */ export interface ChatbotFloatingSchema extends BaseSchema, - Pick { + Pick { type: 'chatbot-floating'; /** * Render assistant messages as markdown inside the panel. Forwarded to the @@ -1969,7 +1986,7 @@ export interface ChatbotFloatingSchema * scores. Every read is filed under the TYPE of the object it is read from; * this declaration carries none. What the renderer DOES read off this node: * `api`, `assistantAvatarFallback`, `assistantAvatarUrl`, `autoResponse`, - * `autoResponseDelay`, `autoResponseText`, `conversationId`, + * `autoResponseDelayMs`, `autoResponseText`, `conversationId`, * `enableFileUpload`, `enableMarkdown`, `floatingConfig`, `headers`, * `messages`, `model`, `onClear`, `onError`, `onSend`, * `placeholder`, `requestBody`, `showTimestamp`, `streamingEnabled`, diff --git a/packages/types/src/crud.ts b/packages/types/src/crud.ts index 188868cd4d..3881a9757e 100644 --- a/packages/types/src/crud.ts +++ b/packages/types/src/crud.ts @@ -247,7 +247,14 @@ export interface ActionSchema extends BaseSchema { /** * Timeout in milliseconds */ - timeout?: number; + timeoutMs?: number; + /** + * RENAMED to {@link ActionSchema.timeoutMs} (objectstack#25762, ADR-0087): a duration + * carries its unit in its key name, never only in its description. The + * value (milliseconds) is unchanged; the zod twin refuses this spelling by name. + * @deprecated Renamed to `timeoutMs`. + */ + timeout?: never; /** * Retry configuration */ @@ -259,7 +266,14 @@ export interface ActionSchema extends BaseSchema { /** * Delay between retries (in ms) */ - delay?: number; + delayMs?: number; + /** + * RENAMED to `delayMs` (objectstack#25762, ADR-0087): a duration + * carries its unit in its key name, never only in its description. The + * value (milliseconds) is unchanged; the zod twin refuses this spelling by name. + * @deprecated Renamed to `delayMs`. + */ + delay?: never; }; } diff --git a/packages/types/src/feedback.ts b/packages/types/src/feedback.ts index 6b4776dc1a..4f5c0f3ed1 100644 --- a/packages/types/src/feedback.ts +++ b/packages/types/src/feedback.ts @@ -298,7 +298,14 @@ export interface ToastSchema extends BaseSchema { * Auto-dismiss duration in milliseconds * @default 5000 */ - duration?: number; + durationMs?: number; + /** + * RENAMED to {@link ToastSchema.durationMs} (objectstack#25762, ADR-0087): a duration + * carries its unit in its key name, never only in its description. The + * value (milliseconds) is unchanged; the zod twin refuses this spelling by name. + * @deprecated Renamed to `durationMs`. + */ + duration?: never; /** * Toast position * @default 'bottom-right' @@ -312,7 +319,7 @@ export interface ToastSchema extends BaseSchema { * published faces whose accept sets were disjoint, one of them empty. An * author who wrote `action` got a green `safeParse` and a `tsc` refusal, and * no spelling satisfied both. The `toast` renderer read NEITHER face — it - * reads `variant`, `title`, `description`, `duration`, `buttonVariant`, + * reads `variant`, `title`, `description`, `durationMs`, `buttonVariant`, * `className` and `buttonLabel`, and nothing else — so nothing could ever have * run it. * @@ -371,7 +378,7 @@ export interface ToastSchema extends BaseSchema { * `packages/plugin-chatbot/src/renderer.tsx` is the kind of prefix hit grep * scores. Every read is filed under the TYPE of the object it is read from; * this declaration carries none. What the renderer DOES read off this node: - * `buttonLabel`, `buttonVariant`, `className`, `description`, `duration`, + * `buttonLabel`, `buttonVariant`, `className`, `description`, `durationMs`, * `title`, `variant` (in * `packages/components/src/renderers/feedback/toast.tsx`). * @@ -398,7 +405,7 @@ export interface ToastSchema extends BaseSchema { * `packages/plugin-chatbot/src/renderer.tsx` is the kind of prefix hit grep * scores. Every read is filed under the TYPE of the object it is read from; * this declaration carries none. What the renderer DOES read off this node: - * `buttonLabel`, `buttonVariant`, `className`, `description`, `duration`, + * `buttonLabel`, `buttonVariant`, `className`, `description`, `durationMs`, * `title`, `variant` (in * `packages/components/src/renderers/feedback/toast.tsx`). * diff --git a/packages/types/src/overlay.ts b/packages/types/src/overlay.ts index 81610d54b4..04c978efcd 100644 --- a/packages/types/src/overlay.ts +++ b/packages/types/src/overlay.ts @@ -824,7 +824,14 @@ export interface TooltipSchema extends BaseSchema { * Delay before showing (ms) * @default 200 */ - delayDuration?: number; + delayDurationMs?: number; + /** + * RENAMED to {@link TooltipSchema.delayDurationMs} (objectstack#25762, ADR-0087): a duration + * carries its unit in its key name, never only in its description. The + * value (milliseconds) is unchanged; the zod twin refuses this spelling by name. + * @deprecated Renamed to `delayDurationMs`. + */ + delayDuration?: never; } /** @@ -865,12 +872,26 @@ export interface HoverCardSchema extends BaseSchema { * Open delay (ms) * @default 200 */ - openDelay?: number; + openDelayMs?: number; /** * Close delay (ms) * @default 300 */ - closeDelay?: number; + closeDelayMs?: number; + /** + * RENAMED to {@link HoverCardSchema.openDelayMs} (objectstack#25762, ADR-0087): a duration + * carries its unit in its key name, never only in its description. The + * value (milliseconds) is unchanged; the zod twin refuses this spelling by name. + * @deprecated Renamed to `openDelayMs`. + */ + openDelay?: never; + /** + * RENAMED to {@link HoverCardSchema.closeDelayMs} (objectstack#25762, ADR-0087): a duration + * carries its unit in its key name, never only in its description. The + * value (milliseconds) is unchanged; the zod twin refuses this spelling by name. + * @deprecated Renamed to `closeDelayMs`. + */ + closeDelay?: never; /** * Alignment of the card against its trigger. * @@ -902,7 +923,7 @@ export interface HoverCardSchema extends BaseSchema { * `packages/plugin-chatbot/src/renderer.tsx` is the kind of prefix hit grep * scores. Every read is filed under the TYPE of the object it is read from; * this declaration carries none. What the renderer DOES read off this node: - * `align`, `closeDelay`, `content`, `openDelay`, `side`, `trigger` (in + * `align`, `closeDelayMs`, `content`, `openDelayMs`, `side`, `trigger` (in * `packages/components/src/renderers/overlay/hover-card.tsx`). * * Before objectui#9256 tombstoned them here, `body` and `children` were both @@ -929,7 +950,7 @@ export interface HoverCardSchema extends BaseSchema { * `packages/plugin-chatbot/src/renderer.tsx` is the kind of prefix hit grep * scores. Every read is filed under the TYPE of the object it is read from; * this declaration carries none. What the renderer DOES read off this node: - * `align`, `closeDelay`, `content`, `openDelay`, `side`, `trigger` (in + * `align`, `closeDelayMs`, `content`, `openDelayMs`, `side`, `trigger` (in * `packages/components/src/renderers/overlay/hover-card.tsx`). * * Before objectui#9256 tombstoned them here, `body` and `children` were both diff --git a/packages/types/src/reports.ts b/packages/types/src/reports.ts index f209413834..2fa55a6a3f 100644 --- a/packages/types/src/reports.ts +++ b/packages/types/src/reports.ts @@ -493,7 +493,15 @@ export interface ReportComponentSchema extends BaseSchema { /** * Auto-refresh interval (in seconds) */ - refreshInterval?: number; + refreshIntervalSeconds?: number; + + /** + * RENAMED to {@link ReportComponentSchema.refreshIntervalSeconds} (objectstack#25762, ADR-0087): + * a duration carries its unit in its key name, never only in its description. + * The value (seconds) is unchanged; the zod twin refuses this spelling by name. + * @deprecated Renamed to `refreshIntervalSeconds`. + */ + refreshInterval?: never; /** * Loading state diff --git a/packages/types/src/zod/complex.zod.ts b/packages/types/src/zod/complex.zod.ts index e8e6fedcd9..3655767a06 100644 --- a/packages/types/src/zod/complex.zod.ts +++ b/packages/types/src/zod/complex.zod.ts @@ -17,7 +17,7 @@ */ import { z } from 'zod'; -import { aliasKeyRefusal, handlerKeyRefusal, retiredNodeType, retirementTombstone } from './tombstone.zod.js'; +import { aliasKeyRefusal, durationKeyRenamedGuidance, handlerKeyRefusal, retiredNodeType, retirementTombstone } from './tombstone.zod.js'; import { ChartTypeSchema as SpecChartTypeSchema, checkDashboardWidgetChartMeasureArity, @@ -740,7 +740,8 @@ export const CarouselSchema = BaseSchema.extend({ .describe("Scroll axis — `orientation={schema.orientation || 'horizontal'}` (objectui#6150)"), // Applied at renderers/complex/carousel.tsx:30 as `className={schema.itemClassName}` on every CarouselItem. itemClassName: z.string().optional().describe('Per-slide Tailwind classes, applied to every carousel item (objectui#6150)'), - autoPlay: z.number().optional().describe('Auto-play interval (ms)'), + autoPlayMs: z.number().optional().describe('Auto-play interval (ms)'), + autoPlay: retirementTombstone(durationKeyRenamedGuidance('autoPlay', 'autoPlayMs', 'milliseconds')), showArrows: z.boolean().optional().describe('Show navigation arrows'), showDots: z.boolean().optional().describe('Show navigation dots'), loop: z.boolean().optional().describe('Enable infinite loop'), @@ -941,7 +942,8 @@ export const ChatbotSchema = BaseSchema.extend({ maxHeight: z.string().optional().describe('Maximum height of the chat message container (CSS value)'), autoResponse: z.boolean().optional().describe('Enable local auto-response (demo/playground) mode'), autoResponseText: z.string().optional().describe('Text of the local auto-response'), - autoResponseDelay: z.number().optional().describe('Delay in milliseconds before the local auto-response is sent'), + autoResponseDelayMs: z.number().optional().describe('Delay in milliseconds before the local auto-response is sent'), + autoResponseDelay: retirementTombstone(durationKeyRenamedGuidance('autoResponseDelay', 'autoResponseDelayMs', 'milliseconds')), onSend: handlerKeyRefusal('onSend', 'runtime-slot', 'Called after a message is sent, in both API and local auto-response mode'), children: retirementTombstone( 'REFUSED (objectui#9256, ADR-0049) — `chatbot` reads NEITHER content channel: measured with the ' @@ -949,7 +951,7 @@ export const ChatbotSchema = BaseSchema.extend({ + '`children` for this node, and `SchemaRenderer` strips both out of the props bag it spreads. An ' + 'authored value therefore rendered NOTHING — no render-time error or warning and no element; only the parser tier\'s `not-a-container` warning (objectui#9910) noticed it. ' + 'What it renders instead: `api`, `assistantAvatarFallback`, `assistantAvatarUrl`, `autoResponse`, ' - + '`autoResponseDelay`, `autoResponseText`, `conversationId`, `headers`, `maxHeight`, ' + + '`autoResponseDelayMs`, `autoResponseText`, `conversationId`, `headers`, `maxHeight`, ' + '`messages`, `model`, `onError`, `onSend`, `placeholder`, `requestBody`, ' + '`showTimestamp`, `streamingEnabled`, `systemPrompt`, `userAvatarFallback`, `userAvatarUrl`.', ), @@ -994,7 +996,7 @@ const ChatbotSharedMirrorShape = ChatbotSchema.pick({ assistantAvatarFallback: true, autoResponse: true, autoResponseText: true, - autoResponseDelay: true, + autoResponseDelayMs: true, onSend: true, }).shape; @@ -1017,6 +1019,7 @@ export const ChatbotEnhancedSchema = BaseSchema.extend({ type: z.literal('chatbot-enhanced'), ...ChatbotSharedMirrorShape, maxToolRoundtrips: ChatbotSchema.shape.maxToolRoundtrips, + autoResponseDelay: ChatbotSchema.shape.autoResponseDelay, maxHeight: ChatbotSchema.shape.maxHeight, processVisibility: ChatbotSchema.shape.processVisibility, enableMarkdown: chatbotEnableMarkdownArm(), @@ -1038,7 +1041,7 @@ export const ChatbotEnhancedSchema = BaseSchema.extend({ + 'authored value therefore rendered NOTHING — no render-time error or warning and no element; only the parser tier\'s `not-a-container` warning (objectui#9910) noticed it. ' + 'The chat API body params go on `requestBody`, which the registration forwards to the chat runtime. ' + 'What it renders instead: `api`, `assistantAvatarFallback`, `assistantAvatarUrl`, `autoResponse`, ' - + '`autoResponseDelay`, `autoResponseText`, `conversationId`, `enableFileUpload`, `enableMarkdown`, ' + + '`autoResponseDelayMs`, `autoResponseText`, `conversationId`, `enableFileUpload`, `enableMarkdown`, ' + '`headers`, `maxHeight`, `messages`, `model`, `onClear`, `onError`, ' + '`onSend`, `placeholder`, `processVisibility`, `requestBody`, `showTimestamp`, ' + '`streamingEnabled`, `surface`, `systemPrompt`, `userAvatarFallback`, `userAvatarUrl`.', @@ -1049,7 +1052,7 @@ export const ChatbotEnhancedSchema = BaseSchema.extend({ + '`children` for this node, and `SchemaRenderer` strips both out of the props bag it spreads. An ' + 'authored value therefore rendered NOTHING — no render-time error or warning and no element; only the parser tier\'s `not-a-container` warning (objectui#9910) noticed it. ' + 'What it renders instead: `api`, `assistantAvatarFallback`, `assistantAvatarUrl`, `autoResponse`, ' - + '`autoResponseDelay`, `autoResponseText`, `conversationId`, `enableFileUpload`, `enableMarkdown`, ' + + '`autoResponseDelayMs`, `autoResponseText`, `conversationId`, `enableFileUpload`, `enableMarkdown`, ' + '`headers`, `maxHeight`, `messages`, `model`, `onClear`, `onError`, ' + '`onSend`, `placeholder`, `processVisibility`, `requestBody`, `showTimestamp`, ' + '`streamingEnabled`, `surface`, `systemPrompt`, `userAvatarFallback`, `userAvatarUrl`.', @@ -1116,6 +1119,7 @@ export const ChatbotFloatingSchema = BaseSchema.extend({ type: z.literal('chatbot-floating'), ...ChatbotSharedMirrorShape, maxToolRoundtrips: ChatbotSchema.shape.maxToolRoundtrips, + autoResponseDelay: ChatbotSchema.shape.autoResponseDelay, enableMarkdown: chatbotEnableMarkdownArm(), enableFileUpload: chatbotEnableFileUploadArm(), onClear: chatbotOnClearArm(), @@ -1135,7 +1139,7 @@ export const ChatbotFloatingSchema = BaseSchema.extend({ + 'authored value therefore rendered NOTHING — no render-time error or warning and no element; only the parser tier\'s `not-a-container` warning (objectui#9910) noticed it. ' + 'The chat API body params go on `requestBody`, which the registration forwards to the chat runtime. ' + 'What it renders instead: `api`, `assistantAvatarFallback`, `assistantAvatarUrl`, `autoResponse`, ' - + '`autoResponseDelay`, `autoResponseText`, `conversationId`, `enableFileUpload`, `enableMarkdown`, ' + + '`autoResponseDelayMs`, `autoResponseText`, `conversationId`, `enableFileUpload`, `enableMarkdown`, ' + '`floatingConfig`, `headers`, `messages`, `model`, `onClear`, `onError`, ' + '`onSend`, `placeholder`, `requestBody`, `showTimestamp`, `streamingEnabled`, `systemPrompt`, ' + '`userAvatarFallback`, `userAvatarUrl`.', @@ -1146,7 +1150,7 @@ export const ChatbotFloatingSchema = BaseSchema.extend({ + '`children` for this node, and `SchemaRenderer` strips both out of the props bag it spreads. An ' + 'authored value therefore rendered NOTHING — no render-time error or warning and no element; only the parser tier\'s `not-a-container` warning (objectui#9910) noticed it. ' + 'What it renders instead: `api`, `assistantAvatarFallback`, `assistantAvatarUrl`, `autoResponse`, ' - + '`autoResponseDelay`, `autoResponseText`, `conversationId`, `enableFileUpload`, `enableMarkdown`, ' + + '`autoResponseDelayMs`, `autoResponseText`, `conversationId`, `enableFileUpload`, `enableMarkdown`, ' + '`floatingConfig`, `headers`, `messages`, `model`, `onClear`, `onError`, ' + '`onSend`, `placeholder`, `requestBody`, `showTimestamp`, `streamingEnabled`, `systemPrompt`, ' + '`userAvatarFallback`, `userAvatarUrl`.', diff --git a/packages/types/src/zod/crud.zod.ts b/packages/types/src/zod/crud.zod.ts index 587eff459f..ab072286c7 100644 --- a/packages/types/src/zod/crud.zod.ts +++ b/packages/types/src/zod/crud.zod.ts @@ -20,7 +20,7 @@ import { z } from 'zod'; import { BaseSchema, SchemaNodeSchema } from './base.zod.js'; -import { handlerKeyRefusal, neitherContentChannelGuidance, retirementTombstone } from './tombstone.zod.js'; +import { durationKeyRenamedGuidance, handlerKeyRefusal, neitherContentChannelGuidance, retirementTombstone } from './tombstone.zod.js'; import type { ActionSchema as ActionDeclaration } from '../crud.js'; /** @@ -139,10 +139,12 @@ export const ActionSchema: z.ZodType = z.l event: z.string().optional().describe('Event name'), metadata: z.record(z.string(), z.any()).optional().describe('Additional metadata'), }).optional().describe('Action logging/tracking'), - timeout: z.number().optional().describe('Timeout in milliseconds'), + timeoutMs: z.number().optional().describe('Timeout in milliseconds'), + timeout: retirementTombstone(durationKeyRenamedGuidance('timeout', 'timeoutMs', 'milliseconds')), retry: z.object({ maxAttempts: z.number().optional().describe('Maximum retry attempts'), - delay: z.number().optional().describe('Delay between retries (in ms)'), + delayMs: z.number().optional().describe('Delay between retries (in ms)'), + delay: retirementTombstone(durationKeyRenamedGuidance('delay', 'delayMs', 'milliseconds')), }).optional().describe('Retry configuration'), })); diff --git a/packages/types/src/zod/feedback.zod.ts b/packages/types/src/zod/feedback.zod.ts index 1174fc5e71..97c9d1932d 100644 --- a/packages/types/src/zod/feedback.zod.ts +++ b/packages/types/src/zod/feedback.zod.ts @@ -17,7 +17,7 @@ */ import { z } from 'zod'; -import { handlerKeyRefusal, retirementTombstone } from './tombstone.zod.js'; +import { durationKeyRenamedGuidance, handlerKeyRefusal, retirementTombstone } from './tombstone.zod.js'; import { BaseSchema, SchemaNodeSchema } from './base.zod.js'; /** @@ -106,7 +106,8 @@ export const ToastSchema = BaseSchema.extend({ title: z.string().optional().describe('Toast title'), description: z.string().optional().describe('Toast description'), variant: z.enum(['default', 'success', 'warning', 'error', 'info']).optional().describe('Toast variant'), - duration: z.number().optional().describe('Auto-dismiss duration (ms)'), + durationMs: z.number().optional().describe('Auto-dismiss duration (ms)'), + duration: retirementTombstone(durationKeyRenamedGuidance('duration', 'durationMs', 'milliseconds')), position: z.enum([ 'top-left', 'top-center', @@ -135,7 +136,7 @@ export const ToastSchema = BaseSchema.extend({ + 'NO replacement spelling and the capability was never fulfilled — objectui#6250 moved the toast ' + 'demos off an in-toast action entirely, and an in-toast action button is a capability expansion ' + 'with zero runtime today. Raise the toast from the node itself (`title`, `description`, ' - + '`variant`, `duration`) and label its trigger with `buttonLabel` / `buttonVariant`.', + + '`variant`, `durationMs`) and label its trigger with `buttonLabel` / `buttonVariant`.', ), onDismiss: handlerKeyRefusal('onDismiss', 'retired', 'Dismiss handler'), // The trigger button the `toast` renderer draws in place. `buttonVariant` @@ -157,7 +158,7 @@ export const ToastSchema = BaseSchema.extend({ + 'TypeScript type checker across all 24 registering packages, no renderer read consumes `body` or ' + '`children` for this node, and `SchemaRenderer` strips both out of the props bag it spreads. An ' + 'authored value therefore rendered NOTHING — no render-time error or warning and no element; only the parser tier\'s `not-a-container` warning (objectui#9910) noticed it. ' - + 'What it renders instead: `buttonLabel`, `buttonVariant`, `className`, `description`, `duration`, ' + + 'What it renders instead: `buttonLabel`, `buttonVariant`, `className`, `description`, `durationMs`, ' + '`title`, `variant`.', ), children: retirementTombstone( @@ -165,7 +166,7 @@ export const ToastSchema = BaseSchema.extend({ + 'TypeScript type checker across all 24 registering packages, no renderer read consumes `body` or ' + '`children` for this node, and `SchemaRenderer` strips both out of the props bag it spreads. An ' + 'authored value therefore rendered NOTHING — no render-time error or warning and no element; only the parser tier\'s `not-a-container` warning (objectui#9910) noticed it. ' - + 'What it renders instead: `buttonLabel`, `buttonVariant`, `className`, `description`, `duration`, ' + + 'What it renders instead: `buttonLabel`, `buttonVariant`, `className`, `description`, `durationMs`, ' + '`title`, `variant`.', ), }); diff --git a/packages/types/src/zod/overlay.zod.ts b/packages/types/src/zod/overlay.zod.ts index 017e0b50cd..fa86bd5262 100644 --- a/packages/types/src/zod/overlay.zod.ts +++ b/packages/types/src/zod/overlay.zod.ts @@ -19,7 +19,7 @@ import { z } from 'zod'; import { BaseSchema, SchemaNodeSchema } from './base.zod.js'; import type { MenuItem } from '../overlay.js'; -import { aliasKeyRefusal, handlerKeyRefusal, retirementTombstone } from './tombstone.zod.js'; +import { aliasKeyRefusal, durationKeyRenamedGuidance, handlerKeyRefusal, retirementTombstone } from './tombstone.zod.js'; /** * Dialog Schema - Dialog/modal component @@ -365,7 +365,8 @@ export const TooltipSchema = BaseSchema.extend({ .describe('Rich tooltip content — the fallback for `content`, published by the registration as the "Rich Content" slot (objectui#6771)'), side: z.enum(['top', 'right', 'bottom', 'left']).optional().describe('Tooltip side'), align: z.enum(['start', 'center', 'end']).optional().describe('Tooltip alignment'), - delayDuration: z.number().optional().describe('Delay before showing (ms)'), + delayDurationMs: z.number().optional().describe('Delay before showing (ms)'), + delayDuration: retirementTombstone(durationKeyRenamedGuidance('delayDuration', 'delayDurationMs', 'milliseconds')), // AN INVERSION, not a widening. objectui#8284 tombstoned `children` here on // the measured ground that the renderer read `content || body` and never // `children`. objectui#6771 changed the READ, not the principle: the renderer @@ -394,22 +395,24 @@ export const HoverCardSchema = BaseSchema.extend({ side: z.enum(['top', 'right', 'bottom', 'left']).optional().describe('Hover card side'), align: z.enum(['start', 'center', 'end']).optional() .describe('Alignment against the trigger, forwarded as `align` on `HoverCardContent`, beside the already-declared `side` (objectui#6150)'), - openDelay: z.number().optional().describe('Delay before opening (ms)'), - closeDelay: z.number().optional().describe('Delay before closing (ms)'), + openDelayMs: z.number().optional().describe('Delay before opening (ms)'), + closeDelayMs: z.number().optional().describe('Delay before closing (ms)'), + openDelay: retirementTombstone(durationKeyRenamedGuidance('openDelay', 'openDelayMs', 'milliseconds')), + closeDelay: retirementTombstone(durationKeyRenamedGuidance('closeDelay', 'closeDelayMs', 'milliseconds')), onOpenChange: handlerKeyRefusal('onOpenChange', 'runtime-slot', 'Open change handler'), body: retirementTombstone( 'REFUSED (objectui#9256, ADR-0049) — `hover-card` reads NEITHER content channel: measured with the ' + 'TypeScript type checker across all 24 registering packages, no renderer read consumes `body` or ' + '`children` for this node, and `SchemaRenderer` strips both out of the props bag it spreads. An ' + 'authored value therefore rendered NOTHING — no render-time error or warning and no element; only the parser tier\'s `not-a-container` warning (objectui#9910) noticed it. ' - + 'What it renders instead: `align`, `closeDelay`, `content`, `openDelay`, `side`, `trigger`.', + + 'What it renders instead: `align`, `closeDelayMs`, `content`, `openDelayMs`, `side`, `trigger`.', ), children: retirementTombstone( 'REFUSED (objectui#9256, ADR-0049) — `hover-card` reads NEITHER content channel: measured with the ' + 'TypeScript type checker across all 24 registering packages, no renderer read consumes `body` or ' + '`children` for this node, and `SchemaRenderer` strips both out of the props bag it spreads. An ' + 'authored value therefore rendered NOTHING — no render-time error or warning and no element; only the parser tier\'s `not-a-container` warning (objectui#9910) noticed it. ' - + 'What it renders instead: `align`, `closeDelay`, `content`, `openDelay`, `side`, `trigger`.', + + 'What it renders instead: `align`, `closeDelayMs`, `content`, `openDelayMs`, `side`, `trigger`.', ), }); diff --git a/packages/types/src/zod/reports.zod.ts b/packages/types/src/zod/reports.zod.ts index 5641fb02ec..fd5d2a68c3 100644 --- a/packages/types/src/zod/reports.zod.ts +++ b/packages/types/src/zod/reports.zod.ts @@ -23,7 +23,7 @@ import { ReportSchema as SpecReportSchema } from '@objectstack/spec/ui'; import { BaseSchema, SchemaNodeSchema } from './base.zod.js'; import { ChartSchema } from './data-display.zod.js'; import { stripImportedDefaults } from './imported-defaults.js'; -import { handlerKeyRefusal, neitherContentChannelGuidance, retirementTombstone } from './tombstone.zod.js'; +import { durationKeyRenamedGuidance, handlerKeyRefusal, neitherContentChannelGuidance, retirementTombstone } from './tombstone.zod.js'; /** * Report Export Format Schema @@ -187,7 +187,8 @@ export const ReportComponentSchema = BaseSchema.extend({ showExportButtons: z.boolean().optional().describe('Show export buttons'), showPrintButton: z.boolean().optional().describe('Show print button'), showScheduleButton: z.boolean().optional().describe('Show schedule button'), - refreshInterval: z.number().optional().describe('Auto-refresh interval (in seconds)'), + refreshIntervalSeconds: z.number().optional().describe('Auto-refresh interval (in seconds)'), + refreshInterval: retirementTombstone(durationKeyRenamedGuidance('refreshInterval', 'refreshIntervalSeconds', 'seconds')), loading: z.boolean().optional().describe('Loading state'), data: z.array(z.any()).optional().describe('Report data'), // objectui#6152 round 3 — declared on the interface and never mirrored here. READ: diff --git a/packages/types/src/zod/tombstone.zod.ts b/packages/types/src/zod/tombstone.zod.ts index da56e22f57..dce8bca7d9 100644 --- a/packages/types/src/zod/tombstone.zod.ts +++ b/packages/types/src/zod/tombstone.zod.ts @@ -241,6 +241,44 @@ export function retiredNodeType(type: string, guidance: string) { .describe(text); } +/** + * The guidance for a DURATION KEY RENAMED so its name carries its unit + * (objectstack#25762, maintainer decision E of the objectui → objectstack merge, + * objectstack#25677). After the merge, `@objectstack/spec`'s + * `check:duration-unit-keys` judges this package's mirrors too, and it refuses a + * numeric key whose unit lives only in its `.describe()` prose — the + * `timeout: 300` that reads as either milliseconds or seconds. objectstack + * renamed its own population the same way (objectstack#15679 / objectstack#15680: + * `…Ms` / `…Seconds`, the unit the describe already stated). Feed the ONE string + * to a {@link retirementTombstone} under the old spelling, beside the renamed key: + * + * ```ts + * durationMs: z.number().optional().describe('Auto-dismiss duration (ms)'), + * duration: retirementTombstone(durationKeyRenamedGuidance('duration', 'durationMs', 'milliseconds')), + * ``` + * + * It is ADR-0087's rename-with-refusal shape adapted to this package: the old + * key stays DECLARED and refuses by name, pointing at the new one, so an author + * who still writes it is told what to write instead rather than having the value + * kept unread by `.passthrough()`. objectui keeps no conversion registry (the + * ADR-0087 D2 table lives in `@objectstack/spec`), so the refusal plus the + * changeset's FROM → TO table is the conversion. Only the key moves; the value + * and its unit are unchanged, and the guidance says so. + * + * @param retired the spelling an author may still write + * @param renamed the unit-carrying spelling that replaces it + * @param unit the unit both spellings carry, spelled out in words + */ +export function durationKeyRenamedGuidance( + retired: string, + renamed: string, + unit: 'milliseconds' | 'seconds', +): string { + return `RENAMED (objectstack#25762, ADR-0087) — \`${retired}\` is now \`${renamed}\`: a duration carries ` + + 'its unit in its key name, never only in its description. ' + + `Rename the key to \`${renamed}\`; the value (${unit}) is unchanged.`; +} + /** * The guidance for a node whose renderer reads NEITHER content channel * (objectui#9256, family D; the E3 residual moved there by the maintainer's