From f8ecd72eff4e7038a6f108ea74f8997771902d9a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 2 Aug 2026 11:18:46 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat(spec)!:=20resolve=20the=20notification?= =?UTF-8?q?-vocabulary=20dual=20sources=20=E2=80=94=20./ui=20Notification(?= =?UTF-8?q?Config)=20removed,=20./system=20NotificationConfig=20removed,?= =?UTF-8?q?=20./api=20keeps=20the=20bare=20names=20(#4610)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The four #4535-C3 baseline rows were the #4411 trap on the notification vocabulary: Notification(Schema) had a second declaration in ./ui diverging from ./api, and NotificationConfig(Schema) had two declarations (./system vs ./ui) that shared nothing but the name. Import-statement-level scan across framework, cloud and objectui: - ./api Notification(Schema) is the live REST inbox-row contract: embedded in ListNotificationsResponseSchema, part of NotificationProtocol, implemented by @objectstack/client, served by the runtime notifications domain, and mirrored by contracts' InboxNotification (ADR-0030: the bell reads this shape). - ./ui Notification(Schema) — a toast/banner instance shape — had zero importers outside its own unit test; objectui pins only the presentation enums (NotificationType/Position/ActionSchema), which stay. - ./system NotificationConfig(Schema) — a channel+template+recipients+ schedule+retryPolicy+tracking wrapper — had zero importers, is wired into no parent schema, predates ADR-0030's accepted delivery model (NotificationService.emit / NotifyConfigSchema / sys_* objects), and advertised unenforced capability (#3197 dead-letter channels). - ./ui NotificationConfig(Schema) — a toaster global config — had zero importers. Disposal (route 1, dead-side delete, v17 major window): the ./ui pair and BOTH NotificationConfig declarations removed; ./api is the sole owner of the bare Notification(Schema) names and NotificationConfig left the export surface entirely. Compile-time pins (typeof import conditional type, #4581 pattern) keep the bare names out of ./ui and ./system; the surviving ./api declaration is already covered by api/protocol.test.ts. dual-source-exports.baseline.json: exactly the 4 named rows removed (28 -> 24). json-schema.manifest: system/NotificationConfig, ui/Notification and ui/NotificationConfig retired deliberately; their 25 authorable-surface rows hand-deleted per the #4458/#4568/#4581/#4603 precedent. api-surface + reference docs regenerated via check:generated --fix (2 proved stale; the api/notification.mdx page folds into api/protocol.mdx where the declaration lives). docs-import-surface baseline (#4595): untouched. Changeset: @objectstack/spec major with FROM -> TO migration lines. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01M9uWvoEp9CoLzYjNExj9sL --- .changeset/notification-dual-source-c3.md | 58 ++++ content/docs/references/api/meta.json | 1 - content/docs/references/api/notification.mdx | 37 --- content/docs/references/api/protocol.mdx | 22 +- .../docs/references/system/notification.mdx | 22 +- content/docs/references/ui/notification.mdx | 47 +-- packages/spec/api-surface.json | 6 - packages/spec/authorable-surface.json | 25 -- .../spec/dual-source-exports.baseline.json | 4 - packages/spec/json-schema.manifest.json | 3 - packages/spec/src/system/notification.test.ts | 306 +----------------- packages/spec/src/system/notification.zod.ts | 162 +--------- packages/spec/src/ui/notification.test.ts | 121 +------ packages/spec/src/ui/notification.zod.ts | 49 +-- 14 files changed, 137 insertions(+), 726 deletions(-) create mode 100644 .changeset/notification-dual-source-c3.md delete mode 100644 content/docs/references/api/notification.mdx diff --git a/.changeset/notification-dual-source-c3.md b/.changeset/notification-dual-source-c3.md new file mode 100644 index 0000000000..696c1b4282 --- /dev/null +++ b/.changeset/notification-dual-source-c3.md @@ -0,0 +1,58 @@ +--- +"@objectstack/spec": major +--- + +feat(spec)!: the notification vocabulary has one owner per name — `@objectstack/spec/ui` no longer exports `Notification(Schema)` / `NotificationConfig(Schema)`, and `@objectstack/spec/system` no longer exports `NotificationConfig(Schema)` (#4610) + +The names `Notification` / `NotificationSchema` (`./api` vs `./ui`) and +`NotificationConfig` / `NotificationConfigSchema` (`./system` vs `./ui`) +each resolved to **two different declarations** depending on the import +path — the #4411 dual-source trap. Resolution (three-repo, +import-statement-level consumer scan: framework, cloud, objectui): + +- **Removed** `NotificationSchema` / `Notification` from + `@objectstack/spec/ui`. This was a toast/banner "notification instance" + shape (`type`/`severity`/`message`/`duration`/`actions`/`position` + ARIA + props) with **zero importers** in all three repos — objectui's toaster + never adopted it. The live contract is `./api`'s `Notification(Schema)`: + the REST inbox row (`id`/`type`/`title`/`body`/`read`/`data`/`actionUrl`/ + `createdAt`) embedded in `ListNotificationsResponseSchema`, served by + `/api/v1/notifications`, implemented by `@objectstack/client`, and + mirrored by `InboxNotification` in `@objectstack/spec/contracts` + (ADR-0030: the bell reads this shape). + - FROM `import { NotificationSchema, type Notification } from '@objectstack/spec/ui'` → + TO `import { NotificationSchema, type Notification } from '@objectstack/spec/api'`. + **Shape change**: the api row is an inbox record, not a presentation + config — the ui shape's `severity` / `duration` / `dismissible` / + `actions` / `position` / ARIA fields do not exist there. For the + presentation vocabulary keep using the ui enums, which are unchanged: + `NotificationTypeSchema`, `NotificationSeveritySchema`, + `NotificationPositionSchema`, `NotificationActionSchema` (+ their + types) still live in `@objectstack/spec/ui`. +- **Removed** `NotificationConfigSchema` / `NotificationConfig` from **both** + `@objectstack/spec/system` and `@objectstack/spec/ui` — the bare name left + the spec export surface entirely. Both declarations had zero importers in + all three repos and were wired into no parent schema. The system side (a + channel + template + recipients + schedule + retryPolicy + tracking + "unified notification management protocol") predates ADR-0030's accepted + delivery architecture and advertised capability the runtime does not + deliver (its channel enum's `push`/`slack`/`teams`/`webhook` dead-letter, + #3197; nothing reads `schedule`/`retryPolicy`/`tracking`). The ui side (a + toaster global config: `defaultPosition`/`defaultDuration`/`maxVisible`/ + `stackDirection`/`pauseOnHover`) was never adopted by objectui. + - FROM `import { NotificationConfigSchema } from '@objectstack/spec/system'` (or `.../ui`) → + TO: no direct replacement. The live delivery vocabulary is + `NotificationService.emit` (`INotificationService`, + `@objectstack/spec/contracts`), the `notify` flow node + (`NotifyConfigSchema`, `@objectstack/spec/automation`) and the + `sys_notification*` platform objects; per-user delivery preferences are + `NotificationPreferences(Schema)` in `@objectstack/spec/api`. +- `@objectstack/spec/api`'s `Notification(Schema)` and + `NotificationPreferences(Schema)` are **unchanged**; `./api` is now the + sole owner of the bare `Notification(Schema)` names. Imports from `./api` + need no migration. `@objectstack/spec/system`'s `NotificationChannel(Schema)`, + `EmailTemplate(Schema)`, `SMSTemplate(Schema)`, `PushNotification(Schema)` + and `InAppNotification(Schema)` are **unchanged**. + +`dual-source-exports.baseline.json` shrinks by exactly these 4 rows (28 → 24, +#4535 C3). diff --git a/content/docs/references/api/meta.json b/content/docs/references/api/meta.json index f689bb447a..ae048e48a9 100644 --- a/content/docs/references/api/meta.json +++ b/content/docs/references/api/meta.json @@ -32,7 +32,6 @@ "export", "identity", "metadata", - "notification", "package-api", "package-registry", "plugin-rest-api", diff --git a/content/docs/references/api/notification.mdx b/content/docs/references/api/notification.mdx deleted file mode 100644 index 9ec7cb48b1..0000000000 --- a/content/docs/references/api/notification.mdx +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: Notification -description: Notification protocol schemas ---- - -{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} - -## TypeScript Usage - -```typescript -import { NotificationSchema } from '@objectstack/spec/api'; -import type { Notification } from '@objectstack/spec/api'; - -// Validate data -const result = NotificationSchema.parse(data); -``` - ---- - -## Notification - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **id** | `string` | ✅ | Notification ID | -| **type** | `string` | ✅ | Notification type | -| **title** | `string` | ✅ | Notification title | -| **body** | `string` | ✅ | Notification body text | -| **read** | `boolean` | ✅ | Whether notification has been read | -| **data** | `Record` | optional | Additional notification data | -| **actionUrl** | `string` | optional | URL to navigate to when clicked | -| **createdAt** | `string` | ✅ | When notification was created | - - ---- - diff --git a/content/docs/references/api/protocol.mdx b/content/docs/references/api/protocol.mdx index 18618fe9d1..604cfcebb2 100644 --- a/content/docs/references/api/protocol.mdx +++ b/content/docs/references/api/protocol.mdx @@ -20,8 +20,8 @@ validation. Each entry is a canonical `ActionDescriptorSchema`. ## TypeScript Usage ```typescript -import { AiAgentCapabilitiesSchema, AiAgentChatRequestSchema, AiAgentSummarySchema, AiAgentsResponseSchema, AiChatRequestSchema, AiChatResponseSchema, AiCompleteRequestSchema, AiConversationSchema, AiMessageSchema, AiModelsResponseSchema, AiPendingActionSchema, AiPendingActionStatusSchema, AiStreamChunkSchema, ApproveAiPendingActionResponseSchema, AutomationActionsResponseSchema, AutomationTriggerRequestSchema, AutomationTriggerResponseSchema, BatchDataRequestSchema, BatchDataResponseSchema, CheckPermissionRequestSchema, CheckPermissionResponseSchema, CreateAiConversationRequestSchema, CreateDataRequestSchema, CreateDataResponseSchema, CreateManyDataRequestSchema, CreateManyDataResponseSchema, CreateViewRequestSchema, CreateViewResponseSchema, DeleteDataRequestSchema, DeleteDataResponseSchema, DeleteManyDataRequestSchema, DeleteManyDataResponseSchema, DeleteMetaItemRequestSchema, DeleteMetaItemResponseSchema, DeleteViewRequestSchema, DeleteViewResponseSchema, FindDataRequestSchema, FindDataResponseSchema, GetDataRequestSchema, GetDataResponseSchema, GetDiscoveryRequestSchema, GetDiscoveryResponseSchema, GetEffectivePermissionsRequestSchema, GetEffectivePermissionsResponseSchema, GetFieldLabelsRequestSchema, GetFieldLabelsResponseSchema, GetLocalesRequestSchema, GetLocalesResponseSchema, GetMetaItemCachedRequestSchema, GetMetaItemCachedResponseSchema, GetMetaItemRequestSchema, GetMetaItemResponseSchema, GetMetaItemsRequestSchema, GetMetaItemsResponseSchema, GetMetaTypesRequestSchema, GetMetaTypesResponseSchema, GetNotificationPreferencesRequestSchema, GetNotificationPreferencesResponseSchema, GetObjectPermissionsRequestSchema, GetObjectPermissionsResponseSchema, GetPresenceRequestSchema, GetPresenceResponseSchema, GetTranslationsRequestSchema, GetTranslationsResponseSchema, GetUiViewRequestSchema, GetUiViewResponseSchema, GetViewRequestSchema, GetViewResponseSchema, HttpFindQueryParamsSchema, ListAiConversationsRequestSchema, ListAiConversationsResponseSchema, ListAiPendingActionsRequestSchema, ListAiPendingActionsResponseSchema, ListNotificationsRequestSchema, ListNotificationsResponseSchema, ListViewsRequestSchema, ListViewsResponseSchema, MarkAllNotificationsReadRequestSchema, MarkAllNotificationsReadResponseSchema, MarkNotificationsReadRequestSchema, MarkNotificationsReadResponseSchema, NotificationPreferencesSchema, RealtimeConnectRequestSchema, RealtimeConnectResponseSchema, RealtimeDisconnectRequestSchema, RealtimeDisconnectResponseSchema, RealtimeSubscribeRequestSchema, RealtimeSubscribeResponseSchema, RealtimeUnsubscribeRequestSchema, RealtimeUnsubscribeResponseSchema, RegisterDeviceRequestSchema, RegisterDeviceResponseSchema, RejectAiPendingActionResponseSchema, SaveMetaItemRequestSchema, SaveMetaItemResponseSchema, SetPresenceRequestSchema, SetPresenceResponseSchema, UnregisterDeviceRequestSchema, UnregisterDeviceResponseSchema, UpdateAiConversationRequestSchema, UpdateDataRequestSchema, UpdateDataResponseSchema, UpdateManyDataRequestSchema, UpdateManyDataResponseSchema, UpdateNotificationPreferencesRequestSchema, UpdateNotificationPreferencesResponseSchema, UpdateViewRequestSchema, UpdateViewResponseSchema } from '@objectstack/spec/api'; -import type { AiAgentCapabilities, AiAgentChatRequest, AiAgentSummary, AiAgentsResponse, AiChatRequest, AiChatResponse, AiCompleteRequest, AiConversation, AiMessage, AiModelsResponse, AiPendingAction, AiPendingActionStatus, AiStreamChunk, ApproveAiPendingActionResponse, AutomationActionsResponse, AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CheckPermissionRequest, CheckPermissionResponse, CreateAiConversationRequest, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, CreateViewRequest, CreateViewResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, DeleteMetaItemRequest, DeleteMetaItemResponse, DeleteViewRequest, DeleteViewResponse, FindDataRequest, FindDataResponse, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetEffectivePermissionsRequest, GetEffectivePermissionsResponse, GetFieldLabelsRequest, GetFieldLabelsResponse, GetLocalesRequest, GetLocalesResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetObjectPermissionsRequest, GetObjectPermissionsResponse, GetPresenceRequest, GetPresenceResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetUiViewResponse, GetViewRequest, GetViewResponse, ListAiConversationsRequest, ListAiConversationsResponse, ListAiPendingActionsRequest, ListAiPendingActionsResponse, ListNotificationsRequest, ListNotificationsResponse, ListViewsRequest, ListViewsResponse, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, NotificationPreferences, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, RejectAiPendingActionResponse, SaveMetaItemRequest, SaveMetaItemResponse, SetPresenceRequest, SetPresenceResponse, UnregisterDeviceRequest, UnregisterDeviceResponse, UpdateAiConversationRequest, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse, UpdateNotificationPreferencesRequest, UpdateNotificationPreferencesResponse, UpdateViewRequest, UpdateViewResponse } from '@objectstack/spec/api'; +import { AiAgentCapabilitiesSchema, AiAgentChatRequestSchema, AiAgentSummarySchema, AiAgentsResponseSchema, AiChatRequestSchema, AiChatResponseSchema, AiCompleteRequestSchema, AiConversationSchema, AiMessageSchema, AiModelsResponseSchema, AiPendingActionSchema, AiPendingActionStatusSchema, AiStreamChunkSchema, ApproveAiPendingActionResponseSchema, AutomationActionsResponseSchema, AutomationTriggerRequestSchema, AutomationTriggerResponseSchema, BatchDataRequestSchema, BatchDataResponseSchema, CheckPermissionRequestSchema, CheckPermissionResponseSchema, CreateAiConversationRequestSchema, CreateDataRequestSchema, CreateDataResponseSchema, CreateManyDataRequestSchema, CreateManyDataResponseSchema, CreateViewRequestSchema, CreateViewResponseSchema, DeleteDataRequestSchema, DeleteDataResponseSchema, DeleteManyDataRequestSchema, DeleteManyDataResponseSchema, DeleteMetaItemRequestSchema, DeleteMetaItemResponseSchema, DeleteViewRequestSchema, DeleteViewResponseSchema, FindDataRequestSchema, FindDataResponseSchema, GetDataRequestSchema, GetDataResponseSchema, GetDiscoveryRequestSchema, GetDiscoveryResponseSchema, GetEffectivePermissionsRequestSchema, GetEffectivePermissionsResponseSchema, GetFieldLabelsRequestSchema, GetFieldLabelsResponseSchema, GetLocalesRequestSchema, GetLocalesResponseSchema, GetMetaItemCachedRequestSchema, GetMetaItemCachedResponseSchema, GetMetaItemRequestSchema, GetMetaItemResponseSchema, GetMetaItemsRequestSchema, GetMetaItemsResponseSchema, GetMetaTypesRequestSchema, GetMetaTypesResponseSchema, GetNotificationPreferencesRequestSchema, GetNotificationPreferencesResponseSchema, GetObjectPermissionsRequestSchema, GetObjectPermissionsResponseSchema, GetPresenceRequestSchema, GetPresenceResponseSchema, GetTranslationsRequestSchema, GetTranslationsResponseSchema, GetUiViewRequestSchema, GetUiViewResponseSchema, GetViewRequestSchema, GetViewResponseSchema, HttpFindQueryParamsSchema, ListAiConversationsRequestSchema, ListAiConversationsResponseSchema, ListAiPendingActionsRequestSchema, ListAiPendingActionsResponseSchema, ListNotificationsRequestSchema, ListNotificationsResponseSchema, ListViewsRequestSchema, ListViewsResponseSchema, MarkAllNotificationsReadRequestSchema, MarkAllNotificationsReadResponseSchema, MarkNotificationsReadRequestSchema, MarkNotificationsReadResponseSchema, NotificationSchema, NotificationPreferencesSchema, RealtimeConnectRequestSchema, RealtimeConnectResponseSchema, RealtimeDisconnectRequestSchema, RealtimeDisconnectResponseSchema, RealtimeSubscribeRequestSchema, RealtimeSubscribeResponseSchema, RealtimeUnsubscribeRequestSchema, RealtimeUnsubscribeResponseSchema, RegisterDeviceRequestSchema, RegisterDeviceResponseSchema, RejectAiPendingActionResponseSchema, SaveMetaItemRequestSchema, SaveMetaItemResponseSchema, SetPresenceRequestSchema, SetPresenceResponseSchema, UnregisterDeviceRequestSchema, UnregisterDeviceResponseSchema, UpdateAiConversationRequestSchema, UpdateDataRequestSchema, UpdateDataResponseSchema, UpdateManyDataRequestSchema, UpdateManyDataResponseSchema, UpdateNotificationPreferencesRequestSchema, UpdateNotificationPreferencesResponseSchema, UpdateViewRequestSchema, UpdateViewResponseSchema } from '@objectstack/spec/api'; +import type { AiAgentCapabilities, AiAgentChatRequest, AiAgentSummary, AiAgentsResponse, AiChatRequest, AiChatResponse, AiCompleteRequest, AiConversation, AiMessage, AiModelsResponse, AiPendingAction, AiPendingActionStatus, AiStreamChunk, ApproveAiPendingActionResponse, AutomationActionsResponse, AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CheckPermissionRequest, CheckPermissionResponse, CreateAiConversationRequest, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, CreateViewRequest, CreateViewResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, DeleteMetaItemRequest, DeleteMetaItemResponse, DeleteViewRequest, DeleteViewResponse, FindDataRequest, FindDataResponse, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetEffectivePermissionsRequest, GetEffectivePermissionsResponse, GetFieldLabelsRequest, GetFieldLabelsResponse, GetLocalesRequest, GetLocalesResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetObjectPermissionsRequest, GetObjectPermissionsResponse, GetPresenceRequest, GetPresenceResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetUiViewResponse, GetViewRequest, GetViewResponse, ListAiConversationsRequest, ListAiConversationsResponse, ListAiPendingActionsRequest, ListAiPendingActionsResponse, ListNotificationsRequest, ListNotificationsResponse, ListViewsRequest, ListViewsResponse, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, Notification, NotificationPreferences, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, RejectAiPendingActionResponse, SaveMetaItemRequest, SaveMetaItemResponse, SetPresenceRequest, SetPresenceResponse, UnregisterDeviceRequest, UnregisterDeviceResponse, UpdateAiConversationRequest, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse, UpdateNotificationPreferencesRequest, UpdateNotificationPreferencesResponse, UpdateViewRequest, UpdateViewResponse } from '@objectstack/spec/api'; // Validate data const result = AiAgentCapabilitiesSchema.parse(data); @@ -1092,6 +1092,24 @@ const result = AiAgentCapabilitiesSchema.parse(data); | **readCount** | `number` | ✅ | Number of notifications marked as read | +--- + +## Notification + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Notification ID | +| **type** | `string` | ✅ | Notification type | +| **title** | `string` | ✅ | Notification title | +| **body** | `string` | ✅ | Notification body text | +| **read** | `boolean` | ✅ | Whether notification has been read | +| **data** | `Record` | optional | Additional notification data | +| **actionUrl** | `string` | optional | URL to navigate to when clicked | +| **createdAt** | `string` | ✅ | When notification was created | + + --- ## NotificationPreferences diff --git a/content/docs/references/system/notification.mdx b/content/docs/references/system/notification.mdx index 0c378c81e5..046cdc4878 100644 --- a/content/docs/references/system/notification.mdx +++ b/content/docs/references/system/notification.mdx @@ -50,8 +50,8 @@ Supports variables for personalization and file attachments. ## TypeScript Usage ```typescript -import { EmailTemplateSchema, InAppNotificationSchema, NotificationChannelSchema, NotificationConfigSchema, PushNotificationSchema, SMSTemplateSchema } from '@objectstack/spec/system'; -import type { EmailTemplate, InAppNotification, NotificationChannel, NotificationConfig, PushNotification, SMSTemplate } from '@objectstack/spec/system'; +import { EmailTemplateSchema, InAppNotificationSchema, NotificationChannelSchema, PushNotificationSchema, SMSTemplateSchema } from '@objectstack/spec/system'; +import type { EmailTemplate, InAppNotification, NotificationChannel, PushNotification, SMSTemplate } from '@objectstack/spec/system'; // Validate data const result = EmailTemplateSchema.parse(data); @@ -106,24 +106,6 @@ Notification delivery channel (implemented today: inbox, email, sms — push/sla * `webhook` ---- - -## NotificationConfig - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **id** | `string` | ✅ | Notification ID | -| **name** | `string` | ✅ | Notification name | -| **channel** | `Enum<'email' \| 'sms' \| 'push' \| 'in-app' \| 'slack' \| 'teams' \| 'webhook'>` | ✅ | Notification channel | -| **template** | `{ id: string; subject: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; body: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; bodyType?: Enum<'text' \| 'html' \| 'markdown'>; … } \| { id: string; message: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; maxLength?: number; variables?: string[] } \| { title: string; body: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; icon?: string; badge?: number; … } \| { title: string; message: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; type: Enum<'info' \| 'success' \| 'warning' \| 'error'>; actionUrl?: string; … }` | ✅ | Notification template | -| **recipients** | `{ to: string[]; cc?: string[]; bcc?: string[] }` | ✅ | Recipients | -| **schedule** | `{ type: Enum<'immediate' \| 'delayed' \| 'scheduled'>; delay?: number; scheduledAt?: number }` | optional | Scheduling | -| **retryPolicy** | `{ enabled?: boolean; maxRetries?: number; backoffStrategy: Enum<'exponential' \| 'linear' \| 'fixed'> }` | optional | Retry policy | -| **tracking** | `{ trackOpens?: boolean; trackClicks?: boolean; trackDelivery?: boolean }` | optional | Tracking configuration | - - --- ## PushNotification diff --git a/content/docs/references/ui/notification.mdx b/content/docs/references/ui/notification.mdx index e872b515d2..0e6a9d275f 100644 --- a/content/docs/references/ui/notification.mdx +++ b/content/docs/references/ui/notification.mdx @@ -16,37 +16,13 @@ Defines the visual presentation style of the notification. ## TypeScript Usage ```typescript -import { NotificationSchema, NotificationActionSchema, NotificationConfigSchema, NotificationPositionSchema, NotificationSeveritySchema, NotificationTypeSchema } from '@objectstack/spec/ui'; -import type { Notification, NotificationAction, NotificationConfig, NotificationPosition, NotificationSeverity, NotificationType } from '@objectstack/spec/ui'; +import { NotificationActionSchema, NotificationPositionSchema, NotificationSeveritySchema, NotificationTypeSchema } from '@objectstack/spec/ui'; +import type { NotificationAction, NotificationPosition, NotificationSeverity, NotificationType } from '@objectstack/spec/ui'; // Validate data -const result = NotificationSchema.parse(data); +const result = NotificationActionSchema.parse(data); ``` ---- - -## Notification - -Notification instance definition - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **type** | `Enum<'toast' \| 'snackbar' \| 'banner' \| 'alert' \| 'inline'>` | ✅ | Notification presentation style | -| **severity** | `Enum<'info' \| 'success' \| 'warning' \| 'error'>` | ✅ | Notification severity level | -| **title** | `string` | optional | Notification title | -| **message** | `string` | ✅ | Notification message body | -| **icon** | `string` | optional | Icon name override | -| **duration** | `number` | optional | Auto-dismiss duration in ms, omit for persistent | -| **dismissible** | `boolean` | ✅ | Allow user to dismiss the notification | -| **actions** | `{ label: string; action: string; variant: Enum<'primary' \| 'secondary' \| 'link'> }[]` | optional | Action buttons | -| **position** | `Enum<'top_left' \| 'top_center' \| 'top_right' \| 'bottom_left' \| 'bottom_center' \| 'bottom_right'>` | optional | Override default position | -| **ariaLabel** | `string` | optional | Accessible label for screen readers (WAI-ARIA aria-label) | -| **ariaDescribedBy** | `string` | optional | ID of element providing additional description (WAI-ARIA aria-describedby) | -| **role** | `string` | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") | - - --- ## NotificationAction @@ -62,23 +38,6 @@ Notification action button | **variant** | `Enum<'primary' \| 'secondary' \| 'link'>` | ✅ | Button variant style | ---- - -## NotificationConfig - -Global notification system configuration - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **defaultPosition** | `Enum<'top_left' \| 'top_center' \| 'top_right' \| 'bottom_left' \| 'bottom_center' \| 'bottom_right'>` | ✅ | Default screen position for notifications | -| **defaultDuration** | `number` | ✅ | Default auto-dismiss duration in ms | -| **maxVisible** | `number` | ✅ | Maximum number of notifications visible at once | -| **stackDirection** | `Enum<'up' \| 'down'>` | ✅ | Stack direction for multiple notifications | -| **pauseOnHover** | `boolean` | ✅ | Pause auto-dismiss timer on hover | - - --- ## NotificationPosition diff --git a/packages/spec/api-surface.json b/packages/spec/api-surface.json index d35ad64268..b607418865 100644 --- a/packages/spec/api-surface.json +++ b/packages/spec/api-surface.json @@ -1110,8 +1110,6 @@ "NavNodeLike (interface)", "NotificationChannel (type)", "NotificationChannelSchema (const)", - "NotificationConfig (type)", - "NotificationConfigSchema (const)", "ORSet (type)", "ORSetElement (type)", "ORSetElementSchema (const)", @@ -3432,14 +3430,10 @@ "NavigationItemInput (type)", "NavigationItemSchema (const)", "NavigationModeSchema (const)", - "Notification (type)", "NotificationAction (type)", "NotificationActionSchema (const)", - "NotificationConfig (type)", - "NotificationConfigSchema (const)", "NotificationPosition (type)", "NotificationPositionSchema (const)", - "NotificationSchema (const)", "NotificationSeverity (type)", "NotificationSeveritySchema (const)", "NotificationType (type)", diff --git a/packages/spec/authorable-surface.json b/packages/spec/authorable-surface.json index e6535c86f0..d85cfbf660 100644 --- a/packages/spec/authorable-surface.json +++ b/packages/spec/authorable-surface.json @@ -6506,14 +6506,6 @@ "system/MutualTLSConfig:ocspUrl", "system/MutualTLSConfig:pinning", "system/MutualTLSConfig:trustedCAs", - "system/NotificationConfig:channel", - "system/NotificationConfig:id", - "system/NotificationConfig:name", - "system/NotificationConfig:recipients", - "system/NotificationConfig:retryPolicy", - "system/NotificationConfig:schedule", - "system/NotificationConfig:template", - "system/NotificationConfig:tracking", "system/ORSet:elements", "system/ORSet:type", "system/ORSetElement:removed", @@ -7837,26 +7829,9 @@ "ui/NavigationContribution:group", "ui/NavigationContribution:items", "ui/NavigationContribution:priority", - "ui/Notification:actions", - "ui/Notification:ariaDescribedBy", - "ui/Notification:ariaLabel", - "ui/Notification:dismissible", - "ui/Notification:duration", - "ui/Notification:icon", - "ui/Notification:message", - "ui/Notification:position", - "ui/Notification:role", - "ui/Notification:severity", - "ui/Notification:title", - "ui/Notification:type", "ui/NotificationAction:action", "ui/NotificationAction:label", "ui/NotificationAction:variant", - "ui/NotificationConfig:defaultDuration", - "ui/NotificationConfig:defaultPosition", - "ui/NotificationConfig:maxVisible", - "ui/NotificationConfig:pauseOnHover", - "ui/NotificationConfig:stackDirection", "ui/NumberFormat:currency", "ui/NumberFormat:maximumFractionDigits", "ui/NumberFormat:minimumFractionDigits", diff --git a/packages/spec/dual-source-exports.baseline.json b/packages/spec/dual-source-exports.baseline.json index fde8e2ce41..fa8f96ea06 100644 --- a/packages/spec/dual-source-exports.baseline.json +++ b/packages/spec/dual-source-exports.baseline.json @@ -15,10 +15,6 @@ "FieldMappingSchema — [./data (const)] ≠ [./integration (const)] ≠ [./shared (const)]", "HttpMethod — [./api, ./shared (type)] ≠ [./ui (type)]", "HttpRequest — [./shared (type)] ≠ [./ui (type)]", - "Notification — [./api (type)] ≠ [./ui (type)]", - "NotificationConfig — [./system (type)] ≠ [./ui (type)]", - "NotificationConfigSchema — [./system (const)] ≠ [./ui (const)]", - "NotificationSchema — [./api (const)] ≠ [./ui (const)]", "PackageDependency — [./cloud (type)] ≠ [./kernel (type)]", "PackageDependencySchema — [./cloud (const)] ≠ [./kernel (const)]", "RateLimitConfig — [./integration (type)] ≠ [./shared (type)]", diff --git a/packages/spec/json-schema.manifest.json b/packages/spec/json-schema.manifest.json index ba5434f476..ebd6d8aa22 100644 --- a/packages/spec/json-schema.manifest.json +++ b/packages/spec/json-schema.manifest.json @@ -1377,7 +1377,6 @@ "system/MultipartUploadConfig", "system/MutualTLSConfig", "system/NotificationChannel", - "system/NotificationConfig", "system/ORSet", "system/ORSetElement", "system/OTComponent", @@ -1609,9 +1608,7 @@ "ui/NavigationContribution", "ui/NavigationItem", "ui/NavigationMode", - "ui/Notification", "ui/NotificationAction", - "ui/NotificationConfig", "ui/NotificationPosition", "ui/NotificationSeverity", "ui/NotificationType", diff --git a/packages/spec/src/system/notification.test.ts b/packages/spec/src/system/notification.test.ts index 3f1abf63ad..7d1bf600cc 100644 --- a/packages/spec/src/system/notification.test.ts +++ b/packages/spec/src/system/notification.test.ts @@ -5,8 +5,6 @@ import { PushNotificationSchema, InAppNotificationSchema, NotificationChannelSchema, - NotificationConfigSchema, - type NotificationConfig, type EmailTemplate, type SMSTemplate, } from './notification.zod'; @@ -266,295 +264,19 @@ describe('NotificationChannelSchema', () => { }); }); -describe('NotificationConfigSchema', () => { - it('should validate email notification config', () => { - const validConfig: NotificationConfig = { - id: 'welcome-email', - name: 'Welcome Email', - channel: 'email', - template: { - id: 'tpl-001', - subject: 'Welcome to ObjectStack', - body: '

Welcome!

', - bodyType: 'html', - }, - recipients: { - to: ['user@example.com'], - }, - }; - - expect(() => NotificationConfigSchema.parse(validConfig)).not.toThrow(); - }); - - it('should validate SMS notification config', () => { - const validConfig = { - id: 'verification-sms', - name: 'Verification SMS', - channel: 'sms', - template: { - id: 'sms-001', - message: 'Your code is {{code}}', - }, - recipients: { - to: ['+1234567890'], - }, - }; - - expect(() => NotificationConfigSchema.parse(validConfig)).not.toThrow(); - }); - - it('should validate push notification config', () => { - const validConfig = { - id: 'push-alert', - name: 'Push Alert', - channel: 'push', - template: { - title: 'New Message', - body: 'You have a new message', - }, - recipients: { - to: ['device_token_123'], - }, - }; - - expect(() => NotificationConfigSchema.parse(validConfig)).not.toThrow(); - }); - - it('should validate in-app notification config', () => { - const validConfig = { - id: 'system-alert', - name: 'System Alert', - channel: 'in-app', - template: { - title: 'Update Available', - message: 'A new version is available', - type: 'info' as const, - }, - recipients: { - to: ['user_123'], - }, - }; - - expect(() => NotificationConfigSchema.parse(validConfig)).not.toThrow(); - }); - - it('should accept CC and BCC recipients', () => { - const config = { - id: 'email-with-cc', - name: 'Email with CC', - channel: 'email', - template: { - id: 'tpl-002', - subject: 'Test', - body: 'Body', - }, - recipients: { - to: ['user@example.com'], - cc: ['manager@example.com'], - bcc: ['archive@example.com'], - }, - }; - - expect(() => NotificationConfigSchema.parse(config)).not.toThrow(); - }); - - it('should validate immediate schedule', () => { - const config = { - id: 'immediate-notification', - name: 'Immediate', - channel: 'email', - template: { - id: 'tpl-003', - subject: 'Test', - body: 'Body', - }, - recipients: { - to: ['user@example.com'], - }, - schedule: { - type: 'immediate' as const, - }, - }; - - expect(() => NotificationConfigSchema.parse(config)).not.toThrow(); - }); - - it('should validate delayed schedule', () => { - const config = { - id: 'delayed-notification', - name: 'Delayed', - channel: 'email', - template: { - id: 'tpl-004', - subject: 'Test', - body: 'Body', - }, - recipients: { - to: ['user@example.com'], - }, - schedule: { - type: 'delayed' as const, - delay: 3600000, // 1 hour - }, - }; - - expect(() => NotificationConfigSchema.parse(config)).not.toThrow(); - }); - - it('should validate scheduled notification', () => { - const config = { - id: 'scheduled-notification', - name: 'Scheduled', - channel: 'email', - template: { - id: 'tpl-005', - subject: 'Test', - body: 'Body', - }, - recipients: { - to: ['user@example.com'], - }, - schedule: { - type: 'scheduled' as const, - scheduledAt: 1704067200000, - }, - }; - - expect(() => NotificationConfigSchema.parse(config)).not.toThrow(); - }); - - it('should validate retry policy with defaults', () => { - const config = { - id: 'notification-with-retry', - name: 'With Retry', - channel: 'email', - template: { - id: 'tpl-006', - subject: 'Test', - body: 'Body', - }, - recipients: { - to: ['user@example.com'], - }, - retryPolicy: { - backoffStrategy: 'exponential' as const, - }, - }; - - const parsed = NotificationConfigSchema.parse(config); - expect(parsed.retryPolicy?.enabled).toBe(true); - expect(parsed.retryPolicy?.maxRetries).toBe(3); - }); - - it('should validate retry policy backoff strategies', () => { - const strategies = ['exponential', 'linear', 'fixed'] as const; - - strategies.forEach((strategy) => { - const config = { - id: `retry-${strategy}`, - name: 'Test', - channel: 'email', - template: { - id: 'tpl-007', - subject: 'Test', - body: 'Body', - }, - recipients: { - to: ['user@example.com'], - }, - retryPolicy: { - backoffStrategy: strategy, - }, - }; - - expect(() => NotificationConfigSchema.parse(config)).not.toThrow(); - }); - }); - - it('should validate tracking configuration with defaults', () => { - const config = { - id: 'notification-with-tracking', - name: 'With Tracking', - channel: 'email', - template: { - id: 'tpl-008', - subject: 'Test', - body: 'Body', - }, - recipients: { - to: ['user@example.com'], - }, - tracking: {}, - }; - - const parsed = NotificationConfigSchema.parse(config); - expect(parsed.tracking?.trackOpens).toBe(false); - expect(parsed.tracking?.trackClicks).toBe(false); - expect(parsed.tracking?.trackDelivery).toBe(true); - }); - - it('should accept custom tracking configuration', () => { - const config = { - id: 'notification-custom-tracking', - name: 'Custom Tracking', - channel: 'email', - template: { - id: 'tpl-009', - subject: 'Test', - body: 'Body', - }, - recipients: { - to: ['user@example.com'], - }, - tracking: { - trackOpens: true, - trackClicks: true, - trackDelivery: true, - }, - }; - - expect(() => NotificationConfigSchema.parse(config)).not.toThrow(); - }); - - it('should validate complete notification config with all options', () => { - const completeConfig: NotificationConfig = { - id: 'complete-notification', - name: 'Complete Notification', - channel: 'email', - template: { - id: 'tpl-complete', - subject: 'Complete Email {{user_name}}', - body: '{{content}}', - bodyType: 'html', - variables: ['user_name', 'content'], - attachments: [ - { - name: 'report.pdf', - url: 'https://example.com/reports/report.pdf', - }, - ], - }, - recipients: { - to: ['user@example.com', 'admin@example.com'], - cc: ['manager@example.com'], - bcc: ['archive@example.com'], - }, - schedule: { - type: 'scheduled', - scheduledAt: 1704067200000, - }, - retryPolicy: { - enabled: true, - maxRetries: 5, - backoffStrategy: 'exponential', - }, - tracking: { - trackOpens: true, - trackClicks: true, - trackDelivery: true, - }, - }; - - expect(() => NotificationConfigSchema.parse(completeConfig)).not.toThrow(); +// Pin: this module no longer declares the bare NotificationConfig names. The +// pin is compile-time (typeof import is type-level only — no runtime barrel +// load): if either bare name is re-added here, the conditional type flips to +// `true` and the `false` assignment fails `tsc --noEmit`. The name left the +// spec export surface entirely (#4610): its ./ui twin was removed in the same +// change, and ADR-0030's delivery vocabulary (NotificationService.emit / +// NotifyConfigSchema / sys_* objects) is the live contract. +describe('NotificationConfig removal (#4610)', () => { + it('does not re-expose the bare NotificationConfig names from ./system', () => { + type SystemNotificationModule = typeof import('./notification.zod'); + const hasConfigSchema: 'NotificationConfigSchema' extends keyof SystemNotificationModule + ? true + : false = false; + expect(hasConfigSchema).toBe(false); }); }); diff --git a/packages/spec/src/system/notification.zod.ts b/packages/spec/src/system/notification.zod.ts index 9c8c6b97ec..5929732016 100644 --- a/packages/spec/src/system/notification.zod.ts +++ b/packages/spec/src/system/notification.zod.ts @@ -232,158 +232,20 @@ export const NotificationChannelSchema = lazySchema(() => z.enum([ 'webhook', ]).describe('Notification delivery channel (implemented today: inbox, email, sms — push/slack/teams/webhook are not yet implemented and dead-letter; see #3197)')); -/** - * Notification Configuration Schema - * - * Unified notification management protocol supporting multiple channels. - * Includes scheduling, retry policies, and delivery tracking. - * - * @example - * ```json - * { - * "id": "welcome-notification", - * "name": "Welcome Email", - * "channel": "email", - * "template": { - * "id": "tpl-001", - * "subject": "Welcome!", - * "body": "

Welcome

", - * "bodyType": "html" - * }, - * "recipients": { - * "to": ["user@example.com"], - * "cc": ["admin@example.com"] - * }, - * "schedule": { - * "type": "immediate" - * }, - * "retryPolicy": { - * "enabled": true, - * "maxRetries": 3, - * "backoffStrategy": "exponential" - * }, - * "tracking": { - * "trackOpens": true, - * "trackClicks": true, - * "trackDelivery": true - * } - * } - * ``` - */ -export const NotificationConfigSchema = lazySchema(() => z.object({ - /** - * Unique identifier for this notification configuration - */ - id: z.string().describe('Notification ID'), - - /** - * Human-readable name for this notification - */ - name: z.string().describe('Notification name'), - - /** - * Delivery channel for the notification - */ - channel: NotificationChannelSchema.describe('Notification channel'), - - /** - * Notification template based on channel type - */ - template: z.union([ - EmailTemplateSchema, - SMSTemplateSchema, - PushNotificationSchema, - InAppNotificationSchema, - ]).describe('Notification template'), - - /** - * Recipient configuration - */ - recipients: z.object({ - /** - * Primary recipients - */ - to: z.array(z.string()).describe('Primary recipients'), - - /** - * CC recipients (email only) - */ - cc: z.array(z.string()).optional().describe('CC recipients'), - - /** - * BCC recipients (email only) - */ - bcc: z.array(z.string()).optional().describe('BCC recipients'), - }).describe('Recipients'), - - /** - * Scheduling configuration - */ - schedule: z.object({ - /** - * Scheduling type - */ - type: z.enum(['immediate', 'delayed', 'scheduled']).describe('Schedule type'), - - /** - * Delay in milliseconds (for delayed type) - */ - delay: z.number().optional().describe('Delay in milliseconds'), - - /** - * Scheduled send time (Unix timestamp in milliseconds) - */ - scheduledAt: z.number().optional().describe('Scheduled timestamp'), - }).optional().describe('Scheduling'), - - /** - * Retry policy for failed deliveries - */ - retryPolicy: z.object({ - /** - * Enable automatic retries - * @default true - */ - enabled: z.boolean().optional().default(true).describe('Enable retries'), - - /** - * Maximum number of retry attempts - * @default 3 - */ - maxRetries: z.number().optional().default(3).describe('Max retry attempts'), - - /** - * Backoff strategy for retries - */ - backoffStrategy: z.enum(['exponential', 'linear', 'fixed']).describe('Backoff strategy'), - }).optional().describe('Retry policy'), - - /** - * Delivery tracking configuration - */ - tracking: z.object({ - /** - * Track when emails are opened - * @default false - */ - trackOpens: z.boolean().optional().default(false).describe('Track opens'), - - /** - * Track when links are clicked - * @default false - */ - trackClicks: z.boolean().optional().default(false).describe('Track clicks'), - - /** - * Track delivery status - * @default true - */ - trackDelivery: z.boolean().optional().default(true).describe('Track delivery'), - }).optional().describe('Tracking configuration'), -})); +// [#4610] `NotificationConfigSchema` / `NotificationConfig` were removed from +// this module (dual-source cleanup, #4535 C3). The "unified notification +// management protocol" wrapper (channel + template + recipients + schedule + +// retryPolicy + tracking) had ZERO consumers across framework, cloud and +// objectui, was wired into no parent schema, and predates ADR-0030's accepted +// delivery architecture: the real vocabulary is `NotificationService.emit` +// (single ingress, `@objectstack/spec/contracts`), the `notify` flow node +// (`NotifyConfigSchema`, `@objectstack/spec/automation`) and the sys_* +// notification objects. Its `./ui` twin was removed in the same change; the +// bare name left the spec export surface entirely. The channel/template +// vocabulary below stays: `NotificationChannel` is re-exported by +// `@objectstack/spec/contracts` and consumed by `service-messaging`. // Type exports -export type NotificationConfig = z.infer; export type NotificationChannel = z.infer; export type EmailTemplate = z.infer; export type SMSTemplate = z.infer; diff --git a/packages/spec/src/ui/notification.test.ts b/packages/spec/src/ui/notification.test.ts index d05894697f..b45a2b3e7d 100644 --- a/packages/spec/src/ui/notification.test.ts +++ b/packages/spec/src/ui/notification.test.ts @@ -4,14 +4,10 @@ import { NotificationSeveritySchema, NotificationPositionSchema, NotificationActionSchema, - NotificationSchema, - NotificationConfigSchema, type NotificationType, type NotificationSeverity, type NotificationPosition, type NotificationAction, - type Notification, - type NotificationConfig, } from './notification.zod'; describe('NotificationTypeSchema', () => { @@ -79,118 +75,33 @@ describe('NotificationActionSchema', () => { }); }); -describe('NotificationSchema', () => { - it('should apply defaults for minimal config', () => { - const result = NotificationSchema.parse({ message: 'Something happened' }); - expect(result.type).toBe('toast'); - expect(result.severity).toBe('info'); - expect(result.dismissible).toBe(true); - }); - - it('should accept full config with title, message, actions, and position', () => { - const notification: Notification = { - type: 'banner', - severity: 'error', - title: 'System Error', - message: 'An unexpected error occurred', - icon: 'error_outline', - duration: 10000, - dismissible: false, - actions: [ - { label: 'Retry', action: 'retry', variant: 'primary' }, - { label: 'Dismiss', action: 'dismiss', variant: 'link' }, - ], - position: 'top_center', - }; - const result = NotificationSchema.parse(notification); - expect(result.type).toBe('banner'); - expect(result.severity).toBe('error'); - expect(result.title).toBe('System Error'); - expect(result.actions).toHaveLength(2); - expect(result.position).toBe('top_center'); - expect(result.dismissible).toBe(false); - }); - - it('should reject missing message', () => { - expect(() => NotificationSchema.parse({ type: 'toast', severity: 'info' })).toThrow(); - }); - - it('should leave optional fields undefined when not provided', () => { - const result = NotificationSchema.parse({ message: 'Hello' }); - expect(result.title).toBeUndefined(); - expect(result.icon).toBeUndefined(); - expect(result.duration).toBeUndefined(); - expect(result.actions).toBeUndefined(); - expect(result.position).toBeUndefined(); - }); -}); - -describe('NotificationConfigSchema', () => { - it('should apply all defaults for empty config', () => { - const result = NotificationConfigSchema.parse({}); - expect(result.defaultPosition).toBe('top_right'); - expect(result.defaultDuration).toBe(5000); - expect(result.maxVisible).toBe(5); - expect(result.stackDirection).toBe('down'); - expect(result.pauseOnHover).toBe(true); - }); - - it('should accept full config override', () => { - const config: NotificationConfig = { - defaultPosition: 'bottom_left', - defaultDuration: 3000, - maxVisible: 3, - stackDirection: 'up', - pauseOnHover: false, - }; - const result = NotificationConfigSchema.parse(config); - expect(result.defaultPosition).toBe('bottom_left'); - expect(result.defaultDuration).toBe(3000); - expect(result.maxVisible).toBe(3); - expect(result.stackDirection).toBe('up'); - expect(result.pauseOnHover).toBe(false); - }); -}); - describe('Type exports', () => { it('should have valid type exports', () => { const type: NotificationType = 'toast'; const severity: NotificationSeverity = 'info'; const position: NotificationPosition = 'top_right'; const action: NotificationAction = { label: 'OK', action: 'confirm', variant: 'primary' }; - const notification: Notification = { type: 'toast', severity: 'info', message: 'Test', dismissible: true }; - const config: NotificationConfig = { defaultPosition: 'top_right', defaultDuration: 5000, maxVisible: 5, stackDirection: 'down', pauseOnHover: true }; expect(type).toBeDefined(); expect(severity).toBeDefined(); expect(position).toBeDefined(); expect(action).toBeDefined(); - expect(notification).toBeDefined(); - expect(config).toBeDefined(); - }); -}); - -describe('ARIA integration', () => { - it('should accept ARIA props on NotificationSchema', () => { - const result = NotificationSchema.parse({ - message: 'File saved', - ariaLabel: 'Success notification', - role: 'alert', - }); - expect(result.ariaLabel).toBe('Success notification'); - expect(result.role).toBe('alert'); - }); - - it('should reject I18n ariaLabel on NotificationSchema', () => { - expect(() => NotificationSchema.parse({ - message: 'Error occurred', - ariaLabel: { key: 'notifications.error_alert', defaultValue: 'Error alert' }, - })).toThrow(); }); - it('should leave ARIA fields undefined when not provided', () => { - const result = NotificationSchema.parse({ message: 'Test' }); - expect(result.ariaLabel).toBeUndefined(); - expect(result.ariaDescribedBy).toBeUndefined(); - expect(result.role).toBeUndefined(); + // Pin: this module no longer declares the bare Notification(Config) names. + // The pin is compile-time (typeof import is type-level only — no runtime + // barrel load): if either bare name is re-added here, the conditional type + // flips to `true` and the `false` assignment fails `tsc --noEmit`. The bare + // `Notification(Schema)` belongs to `@objectstack/spec/api` alone (#4610); + // `NotificationConfig(Schema)` left the spec surface entirely. + it('does not re-expose the bare Notification/NotificationConfig names from ./ui (#4610)', () => { + type UiNotificationModule = typeof import('./notification.zod'); + const hasNotificationSchema: 'NotificationSchema' extends keyof UiNotificationModule + ? true + : false = false; + const hasNotificationConfigSchema: 'NotificationConfigSchema' extends keyof UiNotificationModule + ? true + : false = false; + expect(hasNotificationSchema).toBe(false); + expect(hasNotificationConfigSchema).toBe(false); }); }); diff --git a/packages/spec/src/ui/notification.zod.ts b/packages/spec/src/ui/notification.zod.ts index 0b795e2322..57ba560241 100644 --- a/packages/spec/src/ui/notification.zod.ts +++ b/packages/spec/src/ui/notification.zod.ts @@ -1,7 +1,7 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { z } from 'zod'; -import { I18nLabelSchema, AriaPropsSchema } from './i18n.zod'; +import { I18nLabelSchema } from './i18n.zod'; /** * Notification Type Schema @@ -59,39 +59,14 @@ export const NotificationActionSchema = lazySchema(() => z.object({ export type NotificationAction = z.infer; -/** - * Notification Schema - * Defines a single notification instance with content, behavior, and positioning. - */ -export const NotificationSchema = lazySchema(() => z.object({ - type: NotificationTypeSchema.default('toast').describe('Notification presentation style'), - severity: NotificationSeveritySchema.default('info').describe('Notification severity level'), - title: I18nLabelSchema.optional().describe('Notification title'), - message: I18nLabelSchema.describe('Notification message body'), - icon: z.string().optional().describe('Icon name override'), - duration: z.number().optional().describe('Auto-dismiss duration in ms, omit for persistent'), - dismissible: z.boolean().default(true).describe('Allow user to dismiss the notification'), - actions: z.array(NotificationActionSchema).optional().describe('Action buttons'), - position: NotificationPositionSchema.optional().describe('Override default position'), -}).merge(AriaPropsSchema.partial()).describe('Notification instance definition')); - -export type Notification = z.infer; - -/** - * Notification Config Schema - * Top-level notification system configuration. - */ -export const NotificationConfigSchema = lazySchema(() => z.object({ - defaultPosition: NotificationPositionSchema.default('top_right') - .describe('Default screen position for notifications'), - defaultDuration: z.number().default(5000) - .describe('Default auto-dismiss duration in ms'), - maxVisible: z.number().default(5) - .describe('Maximum number of notifications visible at once'), - stackDirection: z.enum(['up', 'down']).default('down') - .describe('Stack direction for multiple notifications'), - pauseOnHover: z.boolean().default(true) - .describe('Pause auto-dismiss timer on hover'), -}).describe('Global notification system configuration')); - -export type NotificationConfig = z.infer; +// [#4610] `NotificationSchema` / `Notification` and `NotificationConfigSchema` +// / `NotificationConfig` were removed from this module (dual-source cleanup, +// #4535 C3). The `./ui` "notification instance" and "notification system +// config" wrappers had ZERO consumers across framework, cloud and objectui — +// only the presentation vocabulary above (type/severity/position/action) is +// consumed (objectui pins its toaster implementation against it). The bare +// name `Notification(Schema)` now belongs to `@objectstack/spec/api` alone: +// the REST inbox-row contract served by `/api/v1/notifications` (ADR-0030's +// bell reads that shape). `NotificationConfig(Schema)` left the export +// surface entirely — its `./system` twin was equally consumer-free and +// contradicted ADR-0030's accepted delivery model. From 16b89369c2c03e077e9820be7bd4e9183049a5dc Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 2 Aug 2026 12:45:06 +0000 Subject: [PATCH 2/2] =?UTF-8?q?docs(audits):=20=E4=B8=A5=E6=A0=BC=E6=80=A7?= =?UTF-8?q?=E5=8F=B0=E8=B4=A6=E8=B7=9F=E9=9A=8F=20#4610=20=E2=80=94?= =?UTF-8?q?=E2=80=94=20ui/notification.zod.ts=20=E7=AB=99=E7=82=B9=203=20?= =?UTF-8?q?=E2=86=92=201?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 删除 ./ui 的 Notification / NotificationConfig 两个形状后,台账里 `notification.zod.ts` 那行声明的站点数过期(gate 点名 ledger:454: declares 3 site(s), found 1)。把它从「3 ea」的合并行拆出单列为 1, 并写明为何掉了两个站点;`ui/` 章节总计 200 → 198 相应收敛。 check:strictness-ledger 恢复绿(67 文件 / 5 目录,站点数与章节总计均衡); 同 job 的其余源码审计(liveness / empty-state / variant-docs / exported-any / react-declaration-parity / skill-examples)一并复跑通过。 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01M9uWvoEp9CoLzYjNExj9sL --- docs/audits/2026-07-unknown-key-strictness-ledger.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/audits/2026-07-unknown-key-strictness-ledger.md b/docs/audits/2026-07-unknown-key-strictness-ledger.md index 8e8abf2a22..f48c27208c 100644 --- a/docs/audits/2026-07-unknown-key-strictness-ledger.md +++ b/docs/audits/2026-07-unknown-key-strictness-ledger.md @@ -436,7 +436,7 @@ Classification is per the rule above; **(p)** marks a provisional call made from the file's exports/JSDoc rather than a full read — verify before tightening (the #4001 "sharing-rule lesson": candidates, not verdicts). -### `ui/` — 200 sites +### `ui/` — 198 sites | File | Sites | Class | Note / next action | |---|---|---|---| @@ -451,7 +451,8 @@ tightening (the #4001 "sharing-rule lesson": candidates, not verdicts). | `page.zod.ts` | 7 | authorable | partially strict (ADR-0089) | | `chart.zod.ts` / `i18n.zod.ts` / `responsive.zod.ts` | 6+6+4 | authorable (p) | i18n label shapes are wide-open records by design — verify | | `dataset.zod.ts` / `animation.zod.ts` / `dnd.zod.ts` / `keyboard.zod.ts` / `touch.zod.ts` | 4+4+4+4+7 | authorable (p) | interaction configs | -| `notification.zod.ts` / `offline.zod.ts` / `report.zod.ts` | 3 ea | authorable (p) | | +| `offline.zod.ts` / `report.zod.ts` | 3 ea | authorable (p) | | +| `notification.zod.ts` | 1 | authorable (p) | **#4610 dropped two sites** — the `./ui` `Notification` (toast/banner instance) and `NotificationConfig` (toaster global config) shapes were removed: zero importers in all three repos, and both shadowed live names owned elsewhere (`./api` owns the inbox row). What remains is `NotificationActionSchema`, part of the presentation vocabulary the ui entry keeps | | `sharing.zod.ts` | 2 | authorable (p) | public-sharing config | ### `data/` — 164 sites