Files
2026-05-27 23:29:53 +01:00

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_CODES registry via defineCodes("AUDIENCE_RULES", [...]) with stable codes (INVALID_INPUT, SEGMENT_NOT_FOUND, UNKNOWN_OPERATOR, PROVIDER_FAILED).
  • ADDED: AudienceRulesError extends SuaveplanError<AudienceRulesErrorCode> with optional segmentId / tenantId discriminants.
  • ADDED: Zod schemas for SegmentRule, CreateSegmentInput, UpdateSegmentInput enforced at the public store boundary.
  • ADDED: Injectable Clock + IdGenerator on InMemorySegmentStore (defaults: () => Date.now() + a deterministic per-instance counter). new Date() and counter mutation removed from production paths.
  • MODIFIED: SegmentRefresher consumes the genesis typed EventBus<SegmentRefresherEventMap> instead of the upstream untyped EventEmitter. The consumer API (on("segment:refreshed", handler)) is preserved verbatim.
  • MODIFIED: SegmentEvaluator.applyOperator raises AudienceRulesError with UNKNOWN_OPERATOR for an unrecognised operator (upstream silently returned false). The discriminated SegmentOperator union makes the unknown branch reachable only via a Rule cast — 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 scheduled refresh 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 exposed
  • src/segment-evaluator/segment-evaluator.test.ts — every operator, AND/OR chaining, dot-notation resolution, empty-rules / empty-contacts edge cases, unknown-operator raises UNKNOWN_OPERATOR
  • src/segment-refresher/segment-refresher.test.ts — refresh, refreshAll, typed event emission, cache update, undefined for unknown segment
  • src/store/store.test.ts — every interface method, multi-tenant isolation, Zod validation, Clock + IdGenerator injection
  • src/predicates/predicates.test.ts — every factory produces correct rule shape and evaluator matches behave correctly
  • src/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).