7.6 KiB
Migrate @suaveplan/audience-rules
Package: @suaveplan/audience-rules
Status: PROPOSED
Type: new-package (migration from ../suaveplan/packages/universal/audience-rules/)
Design: (none — see specs/audience-rules/spec.md for the normative contract)
Spec delta: ./specs/audience-rules/spec.md
Why
Audience segmentation is a universal-tier primitive consumed by every recipient-selection surface (campaigns, broadcasts, deliveries, notifications). Upstream @suaveplan/audience-rules exists at packages/universal/audience-rules/ on main: it ships a working rule evaluator with 14 operators, AND/OR chaining, dot-notation field resolution, three refresh modes, predicate helpers, an InMemorySegmentStore, and an EventEmitter for refresh notifications. It is functionally complete but enterprise-incomplete — EventEmitter is the legacy @suaveplan/events v1 API (genesis uses typed EventBus<TMap>), there is no structured error class, no defineCodes registry, no Zod boundary, and no injectable Clock / IdGenerator (the in-memory store uses new Date() directly and a numeric counter at module scope).
This migration ports the full public surface to genesis under the §0a parity rules and lifts it to enterprise quality: typed EventBus<SegmentRefresherEventMap> from genesis @suaveplan/events, structured AudienceRulesError extends SuaveplanError<AudienceRulesErrorCode> with stable AUDIENCE_RULES.* code registry, Zod boundary validation on every public input, injectable Clock + IdGenerator on the in-memory store, sibling .md per implementation file, explicit-named feature barrels, 100% Istanbul coverage.
What changes
- ADDED: 17 public exports preserved from upstream's
src/index.ts—SegmentEvaluator,ContactDataProvider,SegmentRefresher,SegmentStore,InMemorySegmentStore, the seven predicate factories (hasTag,channelIs,statusIs,lastActivityBefore,lastActivityAfter,metadataEquals,metadataContains), and the entity / input / result types (ContactData,Segment,SegmentRule,SegmentOperator,LogicalOp,RefreshMode,SegmentEvaluationResult,CreateSegmentInput,UpdateSegmentInput). - ADDED:
AUDIENCE_RULES_CODESregistry viadefineCodes("AUDIENCE_RULES", [...])with stable codes (INVALID_INPUT,SEGMENT_NOT_FOUND,UNKNOWN_OPERATOR,PROVIDER_FAILED). - ADDED:
AudienceRulesError extends SuaveplanError<AudienceRulesErrorCode>with optionalsegmentId/tenantIddiscriminants. - ADDED: Zod schemas for
SegmentRule,CreateSegmentInput,UpdateSegmentInputenforced at the public store boundary. - ADDED: Injectable
Clock+IdGeneratoronInMemorySegmentStore(defaults:() => Date.now()+ a deterministic per-instance counter).new Date()and counter mutation removed from production paths. - MODIFIED:
SegmentRefresherconsumes the genesis typedEventBus<SegmentRefresherEventMap>instead of the upstream untypedEventEmitter. The consumer API (on("segment:refreshed", handler)) is preserved verbatim. - MODIFIED:
SegmentEvaluator.applyOperatorraisesAudienceRulesErrorwithUNKNOWN_OPERATORfor an unrecognised operator (upstream silently returnedfalse). The discriminatedSegmentOperatorunion makes the unknown branch reachable only via aRulecast — the test exercises this case explicitly.
Parity Checklist
| Capability | Upstream | Migrated | Notes |
|---|---|---|---|
SegmentEvaluator + ContactDataProvider |
✅ | ✅ | dot-notation field resolution preserved verbatim |
| 14 comparison operators (eq/neq/gt/gte/lt/lte/contains/not_contains/in/not_in/exists/not_exists/starts_with/ends_with) | ✅ | ✅ | switch retained |
| AND/OR logical chaining | ✅ | ✅ | left-to-right; default and |
| Three refresh modes (realtime/scheduled/manual) | ✅ | ✅ | type-only enum preserved |
| Seven predicate factories | ✅ | ✅ | verbatim |
SegmentStore interface + InMemorySegmentStore |
✅ | ⬆️ | Enhanced: Clock + IdGenerator injection, Zod boundary |
SegmentRefresher + event emission |
✅ | ⬆️ | Enhanced: typed EventBus<SegmentRefresherEventMap> replaces untyped EventEmitter |
Multi-tenant isolation via tenantId |
✅ | ✅ | preserved on Segment and store list |
| Structured error class + code registry | 🚫 | ⬆️ | Added: AudienceRulesError + AUDIENCE_RULES_CODES |
| Zod-validated public boundaries | 🚫 | ⬆️ | Added: Zod schemas on store inputs |
| OTel instrumentation | 🚫 | 🚫 | Scoped out: upstream has none and audience-rules is a lightweight evaluator; instrumentation belongs in api-plugin-audience-rules (HTTP layer) where request-level cardinality is meaningful. |
Out of scope
- SurrealDB / Postgres store adapters — upstream defers these to a hypothetical
api-plugin-audience-rules; genesis matches. - Scheduled-refresh timer wiring — upstream exposes the
scheduledrefresh mode as a type only; the timer driver is a consumer responsibility. Genesis matches. - Real-time push refresh (e.g. CDC-driven re-evaluation) — same reason; consumer responsibility.
Source mapping
| Requirement | Source lines | Notes |
|---|---|---|
| Public surface (17 exports) | src/index.ts:1-26 |
verbatim |
Segment, SegmentRule, SegmentOperator, LogicalOp, RefreshMode, SegmentEvaluationResult, ContactData, CreateSegmentInput, UpdateSegmentInput types |
src/types/segment.types.ts:1-92 |
verbatim shape |
SegmentEvaluator.evaluate + evaluateRule + 14-operator switch + dot-notation resolveField + AND/OR matchesRules |
src/core/segment-evaluator/segment-evaluator.ts:1-141 |
verbatim algorithm |
SegmentRefresher.refresh + refreshAll + event emission |
src/core/segment-refresher/segment-refresher.ts:1-66 |
EventEmitter → typed EventBus |
SegmentStore + InMemorySegmentStore (6 methods) |
src/stores/segment.store.ts:1-90 |
Enhanced with Clock+IdGenerator + Zod |
| Seven predicate factories | src/predicates/predicates.ts:1-44 |
verbatim |
| Universal-tier purity | upstream package.json (no engines, no browser field) |
preserved |
Requirements with no source line
| Requirement | Justification |
|---|---|
AudienceRulesError + AUDIENCE_RULES_CODES registry |
Genesis-wide policy: every package raises a typed error. |
| Zod boundary at the store | Genesis-wide policy: every public input is Zod-validated. |
Injectable Clock + IdGenerator |
Genesis-wide policy: no Date.now() / new Date() / counter mutation in production paths. |
Typed EventBus swap |
Genesis @suaveplan/events ships typed EventBus<TMap>; migration must use it. |
Verification approach
src/error-codes/error-codes.test.ts— registry shape, every code constructible, discriminants exposedsrc/segment-evaluator/segment-evaluator.test.ts— every operator, AND/OR chaining, dot-notation resolution, empty-rules / empty-contacts edge cases, unknown-operator raisesUNKNOWN_OPERATORsrc/segment-refresher/segment-refresher.test.ts—refresh,refreshAll, typed event emission, cache update, undefined for unknown segmentsrc/store/store.test.ts— every interface method, multi-tenant isolation, Zod validation,Clock+IdGeneratorinjectionsrc/predicates/predicates.test.ts— every factory produces correct rule shape and evaluator matches behave correctlysrc/audience-rules.spec-conformance.test.ts— tier purity, barrel shape, subpath exports, error-code registry, runner-import gate
Impact
Affected dependents
bunx turbo ls --filter=...@suaveplan/audience-rules returns no dependents on genesis (no consumer migrated yet).
- No dependents (verified by
bunx turbo ls --filter=...@suaveplan/audience-rules).