# segment-evaluator _(no submodules)_ ## Purpose `segment-evaluator.ts` provides the pure-function rule engine that decides, for a given `Segment` and a tenant's `ContactData[]`, which contacts match. It supports fourteen comparison operators, AND/OR logical chaining, and dot-notation field resolution including numeric array indices (e.g. `channels.0.type`). It is intentionally storage-agnostic — contact data is supplied by an injected `ContactDataProvider`, so the evaluator runs equally well against an in-memory test array, a paginated database cursor, or a remote API. ## Features - 14 operators: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `contains`, `not_contains`, `in`, `not_in`, `exists`, `not_exists`, `starts_with`, `ends_with`. - AND/OR logical chaining via `logicalOp` on each subsequent rule (default `"and"`; first rule's `logicalOp` is ignored). - Dot-notation field resolution with numeric array index access. - Empty-rules segment matches every contact. - `evaluateRule` exposed for ad-hoc rule testing against a single contact. - Unknown operator raises `AudienceRulesError` with `AUDIENCE_RULES.UNKNOWN_OPERATOR` — the discriminated `SegmentOperator` union makes this branch reachable only via a deliberate cast. ## Usage: Basic Example ```ts import { SegmentEvaluator } from "@suaveplan/audience-rules"; const provider = async () => [ { id: "c1", tags: ["vip"], status: "active" }, { id: "c2", tags: ["basic"], status: "active" }, ]; const evaluator = new SegmentEvaluator(provider); const result = await evaluator.evaluate({ id: "seg1", tenantId: "t1", name: "VIPs", rules: [{ field: "tags", operator: "contains", value: "vip" }], refreshMode: "manual", createdAt: new Date(), updatedAt: new Date(), }); // result.contactIds === ["c1"] ``` ## Usage: Advanced Example ```ts import { SegmentEvaluator } from "@suaveplan/audience-rules"; const provider = async () => [ { id: "c1", status: "active", metadata: { country: "US", score: 90 }, channels: [{ type: "email", value: "a@b.com" }], }, ]; const evaluator = new SegmentEvaluator(provider); // "active AND US, OR score >= 95" const result = await evaluator.evaluate({ id: "seg", tenantId: "t1", name: "complex", refreshMode: "manual", rules: [ { field: "status", operator: "eq", value: "active" }, { field: "metadata.country", operator: "eq", value: "US", logicalOp: "and", }, { field: "metadata.score", operator: "gte", value: 95, logicalOp: "or", }, ], createdAt: new Date(), updatedAt: new Date(), }); ``` ## API Reference - `type ContactDataProvider = (tenantId: string) => Promise` - `class SegmentEvaluator` — constructor `(provider: ContactDataProvider)`. - `evaluator.evaluate(segment: Segment): Promise` — fetches contacts via the provider, evaluates every rule against every contact, returns matching IDs + `total` + `evaluatedAt` + `matchRate`. - `evaluator.evaluateRule(contact: ContactData, rule: SegmentRule): boolean` — pure helper; resolves the dot-notation field and applies the operator. - Throws `AudienceRulesError` with `AUDIENCE_RULES.UNKNOWN_OPERATOR` when an operator outside the closed `SegmentOperator` union is supplied. ## Implementation Notes - `resolveField` returns `undefined` for any missing intermediate path segment, so `not_exists` correctly matches deeply-nested missing fields. - Comparison operators apply unchecked casts (`fieldValue as number`) — this matches the upstream behaviour and lets callers compare ISO date strings lexicographically, which is what the predicate helpers `lastActivityBefore` / `lastActivityAfter` rely on. - `matchesRules` is left-to-right and does not short-circuit aggressively; it always evaluates every rule. This keeps the algorithm simple and deterministic; for very large rule lists the cost is `O(rules * contacts)`. - The evaluator is stateless past the injected provider, so the same instance can serve concurrent segment evaluations safely.