> ## Documentation Index
> Fetch the complete documentation index at: https://documentation.clubedge.live/llms.txt
> Use this file to discover all available pages before exploring further.

# Evaluation engine

> How the evaluation engine resolves flags: pipeline order, reasons, deterministic rollouts, context, and supported operators.

The evaluation engine is a pure, deterministic, framework-agnostic module (`@clubedge/evaluation-engine`). It has zero dependencies on NestJS, Redis, or databases — it's a pure function over flag configuration data and an evaluation context.

## Evaluation pipeline

A flag is resolved in strict order. The first matching step determines the result:

| Step | Check                                            | Result if matched                           | Reason code      |
| ---- | ------------------------------------------------ | ------------------------------------------- | ---------------- |
| 1    | Is the flag archived?                            | `{ enabled: false }`                        | `flag_archived`  |
| 2    | Is the flag disabled in this environment?        | `{ enabled: false }`                        | `flag_disabled`  |
| 3    | Does a targeting rule match (in priority order)? | `{ enabled: rule.value }`                   | `targeting_rule` |
| 4    | Does the percentage rollout bucket this user?    | `{ enabled: true }` or `{ enabled: false }` | `rollout`        |
| 5    | No rules, no rollout, flag is on                 | `{ enabled: true }`                         | `flag_enabled`   |

```mermaid theme={null}
flowchart TD
    REQ["Evaluation request<br/>flagKey + environmentId + context"] --> ARCH{"Flag archived?"}
    ARCH -- Yes --> ARCHIVED["Return OFF<br/>(flag_archived)"]
    ARCH -- No --> ENABLED{"Enabled in env?"}
    ENABLED -- No --> DISABLED["Return OFF<br/>(flag_disabled)"]
    ENABLED -- Yes --> RULES{"Targeting rule match?"}
    RULES -- Yes --> RULEVAL["Return rule value<br/>(targeting_rule)"]
    RULES -- No --> ROLL{"Rollout configured?"}
    ROLL -- No --> BASE["Return ON<br/>(flag_enabled)"]
    ROLL -- Yes --> BUCKET["Hash userId/tenantId<br/>bucket = hash % 100"]
    BUCKET --> HIT{bucket < percentage?}
    HIT -- Yes --> ON["Return ON<br/>(rollout)"]
    HIT -- No --> OFF["Return OFF<br/>(rollout)"]
```

### Key ordering details

* **Rules** are evaluated in ascending `priority` order. Lower numbers are evaluated first. The first matching rule wins — remaining rules are not evaluated.
* **Rollout** is only checked after all targeting rules have been evaluated and none matched.
* **Archived check** is the first short-circuit — even an archived flag with matching rules returns `false`.
* **Disabled check** is the second short-circuit — if `isEnabled` is `false` for the environment, no further evaluation occurs.

## Evaluation reasons

| Reason           | Description                                            |
| ---------------- | ------------------------------------------------------ |
| `flag_archived`  | Flag has been archived (soft-deleted)                  |
| `flag_disabled`  | Flag exists but is disabled in the target environment  |
| `targeting_rule` | A targeting rule matched the evaluation context        |
| `rollout`        | A percentage rollout bucketed the subject in or out    |
| `flag_enabled`   | Flag is enabled with no targeting rules and no rollout |

## Deterministic rollout

Rollouts use **FNV-1a hashing** to assign subjects to buckets deterministically:

```typescript theme={null}
// FNV-1a hash
let hash = 2166136261;  // offset basis
for (let i = 0; i < input.length; i++) {
  hash ^= input.charCodeAt(i);  // XOR with each byte
  hash = (hash * 16777619) >>> 0;  // FNV prime, unsigned
}
const percentage = hash % 100;
return percentage < rolloutPercentage;  // enabled if in bucket
```

| Property           | Detail                                                                                                                                                              |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Hash input**     | `userId` (for `per_user`) or `tenantId` (for `per_tenant`)                                                                                                          |
| **Determinism**    | Same subject + same rollout config always yields the same bucket                                                                                                    |
| **Missing key**    | If the seed (`userId`/`tenantId`) is absent, the rollout returns `false` (not in bucket)                                                                            |
| **Flag isolation** | The hash input is just the subject key — different flags with the same user will produce different buckets because the percentage threshold differs per flag config |

## Context attributes

The evaluation context is the set of attributes used for targeting rules and rollout bucketing:

| Attribute          | Type                                          | Used for                                             |
| ------------------ | --------------------------------------------- | ---------------------------------------------------- |
| `userId`           | `string`                                      | Targeting conditions, `per_user` rollout bucketing   |
| `tenantId`         | `string`                                      | Targeting conditions, `per_tenant` rollout bucketing |
| `email`            | `string`                                      | Targeting conditions                                 |
| `country`          | `string`                                      | Targeting conditions                                 |
| `customAttributes` | `Record<string, string \| number \| boolean>` | Arbitrary targeting conditions                       |

A condition's `attribute` field references one of these keys by name (e.g. `"country"`, `"userId"`, or any custom attribute key). Standard attributes (`userId`, `tenantId`, `email`, `country`) are resolved directly; anything else is resolved from `customAttributes`.

## Supported operators

Targeting rule conditions support these comparison operators:

| Operator | Description           | Applies to              |
| -------- | --------------------- | ----------------------- |
| `==`     | Equals                | string, number, boolean |
| `!=`     | Not equals            | string, number, boolean |
| `>`      | Greater than          | number                  |
| `<`      | Less than             | number                  |
| `>=`     | Greater than or equal | number                  |
| `<=`     | Less than or equal    | number                  |

<Note>
  Only the six operators above are supported. String operators like `ends_with`, `starts_with`, `contains`, etc. are **not** available. Use multiple prioritized flat rules with `==` / `!=` to achieve more complex targeting.
</Note>

## Targeting rule evaluation logic

Rules with multiple conditions combine via the rule's `logic` field:

* **`and`** — every condition must match for the rule to fire
* **`or`** — at least one condition must match

<Example>
  `country == "US" AND plan == "premium"` — one rule, `logic: "and"`, two conditions.

  `country == "US" OR plan == "premium"` — one rule, `logic: "or"`, two conditions.
</Example>

<Warning>
  Nested condition groups (e.g. `(A AND B) OR (C AND (D OR E))`) are **not supported**. The schema has no grouping/parent-condition column — conditions belong to a single flat rule. Achieve complex targeting via multiple prioritized rules evaluated in sequence.
</Warning>

## Batch evaluation

The Evaluation API supports batch evaluation of up to 100 flags in a single request. Results are returned in the same order as the `evaluations` array — match by index, not by `flagKey` alone.

**Server-side path** (`POST /sdk/v1/evaluate/batch`) — each item is evaluated with its own context, hitting the same cache + engine pipeline as single evaluation.

**SDK path** — the SDK does **not** support batch evaluation directly. It fetches all configs via `GET /sdk/v1/config` and evaluates each flag once at fetch time (see [SDK configuration](/feature-flags/reference/sdk-config)).
