first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,120 @@
# segment-evaluator
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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<readonly ContactData[]>`
- `class SegmentEvaluator` — constructor `(provider: ContactDataProvider)`.
- `evaluator.evaluate(segment: Segment): Promise<SegmentEvaluationResult>` —
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.