Files
suaveplan/openspec/changes/enterprise-campaigns/proposal.md
2026-05-27 23:29:53 +01:00

202 lines
6.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# OpenSpec Change: enterprise-campaigns
- **Change ID**: enterprise-campaigns
- **Package**: `@suaveplan/campaigns`
- **Category**: universal
- **Target path**: `universal/email/campaigns`
- **Status**: IN PROGRESS
- **Date**: 2026-04-24
- **Reference spec**: `/mnt/s/web/suaveplan/enterprise/campaigns.md`
---
## Summary
Deliver `@suaveplan/campaigns` on the genesis branch from scratch at enterprise quality. The package provides email campaign orchestration: campaign lifecycle state machine (`draft` → `scheduled` → `sending` → `sent` / `cancelled` / `failed`), audience resolution via `@suaveplan/audience-rules`, template selection via `@suaveplan/email-templates`, per-recipient variable injection, send-rate throttling with configurable concurrency, schedule management (immediate, future ISO-8601, recurring via cron expression), campaign analytics aggregation, and A/B variant support. Genesis adds structured errors, Zod schemas, OTel instrumentation, `AbortSignal` cancellation, idempotent send via `@suaveplan/idempotency`, and retry with circuit breaker via `@suaveplan/retry`.
---
## Current-state audit (genesis)
The package does not exist on genesis yet — this is a fresh build.
---
## Available workspace deps on genesis
| Package | Role |
|---|---|
| `@suaveplan/error` (`workspace:^`) | `CampaignError`; `CAMPAIGN.*` error codes |
| `@suaveplan/types` (`workspace:^`) | `LoggerLike`, `Clock`, `EventBusLike` |
| `@suaveplan/events` (`workspace:^`) | Campaign lifecycle event bus |
| `@suaveplan/email-templates` (`workspace:^`) | Template rendering per recipient |
| `@suaveplan/audience-rules` (`workspace:^`) | Audience segment resolution |
| `@suaveplan/idempotency` (`workspace:^`) | Per-recipient idempotency key guard |
| `@suaveplan/retry` (`workspace:^`) | Retry + circuit breaker for delivery calls |
| `@suaveplan/suppression` (`workspace:^`) | Suppression list check before send |
| `zod` (`catalog:forms`) | Option schema validation |
| `@opentelemetry/api` (`catalog:observability`) | Spans + counters + histograms |
Dev deps: `vitest`, `@vitest/coverage-istanbul`, `fast-check` (all `catalog:testing`).
---
## Public API surface
### Core types
```ts
type CampaignStatus =
| 'draft' | 'scheduled' | 'sending' | 'sent' | 'cancelled' | 'failed'
interface Campaign {
id: string
name: string
templateName: string
audienceRuleId: string
schedule: CampaignSchedule
status: CampaignStatus
abVariants?: AbVariant[]
metadata: Record<string, unknown>
createdAt: Date
updatedAt: Date
}
interface CampaignSchedule {
type: 'immediate' | 'at' | 'recurring'
at?: string // ISO-8601 for type:'at'
cron?: string // cron expression for type:'recurring'
timezone?: string
}
interface AbVariant {
name: string
templateName: string
fraction: number // 0–1, fractions must sum to ≤1
}
interface CampaignSendOptions {
concurrency?: number // max parallel sends; default 10
ratePerSecond?: number // max sends/second; default 100
signal?: AbortSignal
dryRun?: boolean
}
interface CampaignAnalytics {
campaignId: string
sent: number
failed: number
suppressed: number
durationMs: number
}
```
### `CampaignManager`
```ts
class CampaignManager {
constructor(options: CampaignManagerOptions)
create(draft: Omit<Campaign, 'id' | 'status' | 'createdAt' | 'updatedAt'>): Campaign
schedule(id: string, schedule: CampaignSchedule): Campaign
send(id: string, options?: CampaignSendOptions): Promise<CampaignAnalytics>
cancel(id: string): Campaign
get(id: string): Campaign | undefined
list(filter?: CampaignFilter): Campaign[]
}
interface CampaignManagerOptions {
sender: CampaignSender // injectable delivery adapter
clock?: Clock
logger?: LoggerLike
bus?: EventBusLike
}
interface CampaignSender {
send(recipient: string, subject: string, html: string, text: string): Promise<void>
}
```
### Error codes
```ts
type CampaignErrorCode =
| 'CAMPAIGN_NOT_FOUND'
| 'CAMPAIGN_INVALID_TRANSITION'
| 'CAMPAIGN_SEND_FAILED'
| 'CAMPAIGN_TEMPLATE_NOT_FOUND'
| 'CAMPAIGN_AUDIENCE_FAILED'
| 'CAMPAIGN_ABORTED'
```
---
## Genesis additions
1. **`CampaignError`** extends `SuaveplanError` with six stable codes.
2. **Zod schemas** for `Campaign`, `CampaignSchedule`, `AbVariant`, `CampaignSendOptions`, `CampaignAnalytics`.
3. **A/B variant support** — variants are bucketed deterministically by recipient hash (FNV-1a) against configured fractions.
4. **Suppression list integration** — calls `@suaveplan/suppression` before every send; suppressed recipients are counted in analytics without being sent to.
5. **OTel instrumentation** — span `campaign.send`, counter `campaign.send_total`, histogram `campaign.send_duration_ms`, attributes `campaign.id`, `campaign.status`, `campaign.recipient_count`.
6. **Property-based tests** — A/B bucketing distribution, state machine transition invariants.
---
## File layout
```text
universal/email/campaigns/
package.json
tsconfig.json
vitest.config.ts
src/
index.ts
error-codes.ts
constants/
index.ts
types/
common.ts
common.md
core/
campaign-manager/
campaign-manager.ts
campaign-manager.md
campaign-manager.types.ts
campaign-manager.test.ts
index.ts
campaign-fsm/
campaign-fsm.ts
campaign-fsm.md
campaign-fsm.test.ts
index.ts
ab-bucketing/
ab-bucketing.ts
ab-bucketing.md
ab-bucketing.test.ts
index.ts
scheduling/
schedule-parser/
schedule-parser.ts
schedule-parser.md
schedule-parser.test.ts
index.ts
analytics/
campaign-analytics/
campaign-analytics.ts
campaign-analytics.md
campaign-analytics.test.ts
index.ts
__benchmarks__/
campaigns.bench.ts
tests/
integration.test.ts
property.test.ts
```
---
## Impact
**Downstream consumers**: any server-side campaign dispatch service.
**Workspace dep prerequisites**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`, `@suaveplan/email-templates`, `@suaveplan/audience-rules`, `@suaveplan/idempotency`, `@suaveplan/retry`, `@suaveplan/suppression`.