Initial Commit
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
# 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`).
|
||||
Reference in New Issue
Block a user