# OpenSpec Change: enterprise-gaming-chat - **Change ID**: enterprise-gaming-chat - **Package**: `@suaveplan/chat` - **Category**: gaming/multiplayer - **Target path**: `gaming/multiplayer/chat` - **Status**: PROPOSED - **Date**: 2026-04-24 - **Reference spec**: `/mnt/s/web/suaveplan/enterprise/chat.md` --- ## Summary Deliver `@suaveplan/chat` on the genesis branch from scratch at enterprise quality. The package provides in-game chat with named channels, whispers, configurable rate limiting, and a profanity filter. A `ChatManager` maintains typed `ChatChannel` objects (global, local, party, trade, custom); messages are validated against rate limit before dispatch; a `ProfanityFilter` uses a blocklist with configurable mask character; a `WhisperRouter` routes directed messages between two players. Genesis adds Zod message schemas, structured errors, OTel message-rate counter, injectable clock, and 100% coverage. --- ## Current-state audit (genesis) Package does not exist on genesis yet — fresh build. --- ## Available workspace deps on genesis | Package | Role | |---|---| | `@suaveplan/error` (`workspace:^`) | `ChatError`; `CHAT.*` codes | | `@suaveplan/types` (`workspace:^`) | `Clock` | | `@suaveplan/events` (`workspace:^`) | `chat:message`, `chat:whisper`, `chat:muted` events | | `zod` (`catalog:forms`) | `ChatMessage`, `ChannelConfig` schemas | | `@opentelemetry/api` (`catalog:observability`) | Messages-per-second counter | --- ## Public API surface ```ts type ChannelType = 'global' | 'local' | 'party' | 'trade' | 'system' | 'custom' interface ChannelConfig { id: string type: ChannelType displayName: string maxHistory?: number // message ring buffer size, default 100 rateLimitPerMin?: number // default 30 } interface ChatMessage { id: string channelId: string senderId: string senderName: string content: string timestamp: number filtered?: boolean } function createChatManager(options?: ChatManagerOptions): ChatManager interface ChatManagerOptions { profanityFilter?: ProfanityFilter clock?: Clock } interface ChatManager { createChannel(config: ChannelConfig): ChatChannel getChannel(id: string): ChatChannel | undefined deleteChannel(id: string): void sendMessage(channelId: string, senderId: string, senderName: string, content: string): ChatMessage whisper(fromId: string, fromName: string, toId: string, content: string): ChatMessage mute(playerId: string, durationMs?: number): void unmute(playerId: string): void isMuted(playerId: string): boolean tick(dt: number): void // expire mutes } interface ChatChannel { readonly id: string readonly config: ChannelConfig getHistory(): ChatMessage[] subscribe(playerId: string): void unsubscribe(playerId: string): void readonly subscribers: ReadonlySet } // Profanity filter function createProfanityFilter(blocklist: string[], options?: FilterOptions): ProfanityFilter interface FilterOptions { maskChar?: string // default '*' mode?: 'mask' | 'block' // block throws, mask replaces } interface ProfanityFilter { filter(text: string): { filtered: string; triggered: boolean } addWord(word: string): void removeWord(word: string): void } ``` ### Error codes ```ts type ChatErrorCode = | 'CHAT_CHANNEL_NOT_FOUND' | 'CHAT_RATE_LIMIT_EXCEEDED' | 'CHAT_SENDER_MUTED' | 'CHAT_CONTENT_BLOCKED' | 'CHAT_RECIPIENT_NOT_FOUND' ``` --- ## File layout ```text gaming/multiplayer/chat/ package.json tsconfig.json vitest.config.ts src/ index.ts error-codes.ts constants/index.ts types/common.ts types/common.md core/ chat-manager/ chat-manager.ts chat-manager.md chat-manager.types.ts chat-manager.test.ts index.ts chat-channel/ chat-channel.ts chat-channel.md chat-channel.types.ts chat-channel.test.ts index.ts utils/ profanity-filter.ts profanity-filter.md profanity-filter.test.ts rate-limiter.ts rate-limiter.md rate-limiter.test.ts tests/ integration.test.ts ``` --- ## Impact **Downstream**: `@suaveplan/react-social-game` renders chat panel. Prerequisites: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`.