# Flag evaluation, version 1

This is the language-neutral definition of how flag evaluates a flag for a context. The Rust crate
`crates/flag-eval` is the reference implementation; every SDK implements this document and must pass the
conformance vectors in `spec/vectors/` exactly:

- `evaluation.json`: `bucketing` (salt, key, unit → bucket) and `suites`, each a snapshot plus cases
  (flag key, context JSON → expected result).
- `redaction.json`: private-attribute redaction.
- `envelope.json`: opening an encrypted server snapshot (see `sdk-protocol.md`).

All JSON is UTF-8. "String" means a JSON string; "number" a JSON number, compared as an IEEE 754 double.

## Snapshot

```json
{
  "envId": "env_...",
  "version": 42,
  "hashedTargets": false,
  "flags": { "<flag key>": Flag },
  "segments": { "<segment key>": Segment }
}
```

Unknown fields anywhere must be ignored, so the server can add fields without breaking older SDKs.

### Flag

| Field | Type | Meaning |
|---|---|---|
| `key` | string | The flag key. |
| `version` | integer | Flag version, reported in events. |
| `on` | boolean | Targeting on. Default false. |
| `variations` | array of JSON values | The possible values. Results name them by index. |
| `offVariation` | integer, optional | Served when the flag is off or a prerequisite fails. |
| `fallthrough` | Serve | Served when no target or rule matches. |
| `targets` | array of Target | Individual targets, checked in order. |
| `rules` | array of Rule | Checked in order after targets. |
| `prerequisites` | array of `{key, variation}` | Flags that must be on and serve that variation. |
| `salt` | string | Bucketing salt. |
| `clientSide` | boolean | Visible to client-side keys. |
| `trackEvents` | boolean | SDKs send a full `feature` event per evaluation. |

**Serve** is `{"variation": i}` or `{"rollout": Rollout}`. If both are present the rollout wins. If neither is,
the flag is malformed.

**Target**: `{"contextKind": "user", "variation": i, "values": ["key", ...]}`. `contextKind` defaults to `"user"`.

**Rule**: `{"id": "...", "clauses": [Clause, ...], ...Serve, "trackEvents": false}`. A rule matches when every clause
matches; a rule without clauses matches every context. The Serve fields (`variation`, `rollout`) sit directly in
the rule object.

**Rollout**:

```json
{
  "variations": [{"variation": 0, "weight": 50000}, {"variation": 1, "weight": 50000}],
  "bucketBy": "key",
  "contextKind": "user",
  "seed": "optional; replaces the flag's salt",
  "experiment": {
    "id": "exp_...",
    "defaultVariation": 0,
    "layer":   {"key": "...", "salt": "...", "start": 0, "end": 50000},
    "holdout": {"key": "...", "salt": "...", "start": 0, "end": 5000}
  }
}
```

Weights are integers in units of 1/100,000. `bucketBy` defaults to `"key"`, `contextKind` to `"user"`.

### Segment

| Field | Type | Meaning |
|---|---|---|
| `key`, `version`, `salt` | | As for flags. |
| `included`, `excluded` | array of string | User-kind keys always in / always out. |
| `includedContexts`, `excludedContexts` | array of `{contextKind, values}` | The same for any kind. |
| `rules` | array of SegmentRule | `{"id", "clauses", "weight"?, "bucketBy": "key", "rolloutContextKind": "user"}` |

## Contexts

A context is a JSON object.

- **Single kind**: `{"kind": "user", "key": "u1", "<attribute>": value, ..., "_meta": {...}}`. `kind` defaults to
  `"user"`.
- **Multi kind**: `{"kind": "multi", "<kind>": {single context without kind}, ...}`, at least one kind.

A context is invalid, and evaluation returns the error `INVALID_CONTEXT`, if it is not an object, `kind` is not a
string, a kind name is empty, is `"kind"` or `"multi"`, or has characters other than ASCII letters, digits, `.`,
`_` and `-`, or any kind lacks a non-empty string `key`. The attributes of a kind are its fields except `kind` and
`_meta`; `key` is an attribute.

**Attribute references.** A name that does not start with `/` names a top-level attribute literally (so `a/b` is
the attribute called `a/b`). A name that starts with `/` is a path: split the rest on `/`, replace `~1` with `/`
and then `~0` with `~` in each part, take the first part as a top-level attribute and each further part as a
field of a JSON object (anything else: missing). A JSON `null` value counts as missing.

### Private attributes

Before a context leaves an SDK in an event, the SDK removes private attributes: the names configured globally
on the SDK, then those in the context's `_meta.privateAttributes`, each a reference as above. `key` and `kind`
are never removed. The context's `_meta` is replaced by `{"redactedAttributes": [references actually removed, in
that order]}`, or dropped if nothing was removed. In a multi context each kind is redacted on its own.

## Evaluation

`evaluate(snapshot, flagKey, context)` returns `{value, variation, reason, experimentId?}`. `value` is the
variation's value, or `null` with `variation: null` when the caller's default must be served.

1. If the snapshot has no flag `flagKey`: error `FLAG_NOT_FOUND`.
2. If the context is invalid: error `INVALID_CONTEXT`.
3. Evaluate the flag:
   1. If the flag is on the stack of flags being evaluated (a prerequisite cycle): error `MALFORMED_FLAG`.
   2. If `on` is false: serve `offVariation` with reason `OFF` (or `null` and no variation, reason `OFF`).
   3. For each prerequisite in order: if the snapshot lacks the flag, or evaluating it (recursively, with this
      context) does not give the required variation, or that flag is not `on`, serve the off variation with reason
      `PREREQUISITE_FAILED` and `prerequisiteKey`. If the prerequisite's evaluation is a `MALFORMED_FLAG` error,
      return that error.
   4. For each target in order: if the context has the target's kind and that kind's key is in `values`, serve
      the target's variation, reason `TARGET_MATCH`.
   5. For each rule in order (index `i`): if it matches, serve it, reason `RULE_MATCH` with `ruleIndex: i` and
      `ruleId`.
   6. Serve `fallthrough`, reason `FALLTHROUGH`.
4. Serving a variation index that is not in `variations` (including `offVariation`): error `MALFORMED_FLAG`.

An error result is `{"value": null, "variation": null, "reason": {"kind": "ERROR", "errorKind": "..."}}`.
SDKs add `WRONG_TYPE` when a typed accessor (boolean, string, number, object) gets a value of another JSON type;
they return the caller's default for every error.

### Clauses

```json
{"contextKind": "user", "attribute": "email", "op": "endsWith", "values": ["@acme.com"], "negate": false}
```

- `segmentMatch`: matches if the context is in any segment listed in `values` (non-strings and unknown segments
  are skipped). `negate` inverts the result. `attribute` and `contextKind` are ignored.
- An operator not listed below never matches, whatever `negate` says.
- Attribute `kind`: the actual value is the array of the context's kind names (any `contextKind`).
- Otherwise, if the context lacks `contextKind` or the attribute is missing, the clause does **not** match,
  whatever `negate` says.
- If the actual value is an array, the clause matches when any element matches; otherwise when the value
  matches. A value matches when the operator holds for it and any of `values`. Then `negate` inverts the result.

| `op` | Holds when |
|---|---|
| `in` | JSON equality; numbers compare by value (`30` equals `30.0`); arrays and objects deeply. |
| `startsWith`, `endsWith`, `contains` | Both are strings and the string test holds (case-sensitive, by code point). |
| `matches` | The actual value is a string and the clause value, a regular expression, is found anywhere in it. |
| `lessThan`, `lessThanOrEqual`, `greaterThan`, `greaterThanOrEqual` | Both are numbers (no string coercion). |
| `before`, `after` | Both are dates and actual is strictly earlier / later. |
| `semVerEqual`, `semVerLessThan`, `semVerGreaterThan` | Both are strings that parse as versions, compared by precedence. |
| `segmentMatch` | See above. |

**Regular expressions** use the syntax common to RE2, ECMAScript and Python (no look-around, no backreferences)
with RE2's meaning: the shorthand classes are ASCII (`\d` is `[0-9]`, `\w` is `[0-9A-Za-z_]`, `\s` is
`[\t\n\f\r ]`, `\b` and `\B` are ASCII word boundaries, `\D` `\W` `\S` their negations, inside and outside
character classes), `.` matches one Unicode code point other than `\n`, `$` matches only at the very end, and
escaped punctuation (`\-`, `\:`) is that character. Engines that differ
rewrite the pattern first (the Rust implementation, ECMAScript's `\s`, Python's `$` and `\s`). A value that is not
a string or not a valid pattern never matches.

**Dates** are numbers (Unix milliseconds, floored) or strings in exactly the form
`YYYY-MM-DDTHH:MM:SS[.fraction](Z|+HH:MM|-HH:MM)`, where `T` and `Z` may be lowercase. Fractions of any length are
truncated to milliseconds. Field ranges are checked (days per month with leap years, hours 0-23, minutes and
seconds 0-59, offsets up to 23:59). Anything else is not a date.

**Versions** follow Semantic Versioning 2.0.0, except that the minor and patch numbers may be left out (`2` is
`2.0.0`, `2.1` is `2.1.0`). No leading `v`, no leading zeros in numbers or numeric pre-release identifiers. Build
metadata (`+...`) must be well-formed and is ignored. Precedence: major, minor, patch numerically; a version
with a pre-release is lower than the same version without; pre-release identifiers compare left to right,
numeric ones numerically and below alphanumeric ones, alphanumeric ones by ASCII order; a shorter list that is a
prefix of a longer one is lower.

### Segments

A context is in a segment when, in order:

1. its user-kind key is in `included`, or for some `includedContexts` entry the context's key of that kind is in
   `values`: **in**;
2. its user-kind key is in `excluded`, or the same for `excludedContexts`: **out**;
3. some rule's clauses all match and, if the rule has a `weight`, the bucket of the context's
   `rolloutContextKind`/`bucketBy` unit under (segment `salt`, segment `key`) is below `weight`: **in**;
4. otherwise **out**.

Segments may test other segments (`segmentMatch`); a segment already being evaluated does not match.

### Hashed targets

When the snapshot has `hashedTargets: true` (client-safe snapshots), every key in target `values`, segment
`included`/`excluded` and segment context `values` is the lowercase hex SHA-256 of the UTF-8 key. Compare the
hash of the context's key instead of the key. Clause values are never hashed.

## Bucketing

```
bucket(salt, key, unit) = floor(H * 100000 / 2^64)
H = the first 8 bytes of SHA-256(UTF-8 of salt + "." + key + "." + unit), as a big-endian unsigned 64-bit integer
```

Compute it with exact integer arithmetic (128-bit, big integers or `mulhi`): doubles lose bits.

The **unit** is the context's attribute `bucketBy` of kind `contextKind`: a string as is, or a number with no
fractional part and magnitude at most 2^53 written in decimal without exponent or fraction (`12345.0` is
`"12345"`). Any other value, a missing attribute or a missing kind leaves no unit.

**Rollouts** (no `experiment`): with `salt` = the rollout's `seed` if present, else the flag's `salt`, take
`b = bucket(salt, flagKey, unit)`, or 0 when there is no unit. Walk `variations` adding up weights; serve the first
whose running sum exceeds `b`. If none does (weights short of 100,000), serve the last one. An empty list is
`MALFORMED_FLAG`.

**Experiments** (the rollout has `experiment`). The context is in the experiment only if all hold, else it gets
`experiment.defaultVariation` and is not in the experiment:

1. it has a unit;
2. if `holdout` is set: `bucket(holdout.salt, holdout.key, unit)` is **not** in `[start, end)`;
3. if `layer` is set: `bucket(layer.salt, layer.key, unit)` **is** in `[start, end)`;
4. walking `variations` as above with `b = bucket(salt, flagKey, unit)`, some running sum exceeds `b` (weights
   may add up to less than 100,000: the rest is outside the traffic allocation).

In the experiment, the reason gets `"inExperiment": true` and the result `"experimentId"`. SDKs send an exposure
event for such results.

## Reasons

```json
{"kind": "OFF"}
{"kind": "TARGET_MATCH"}
{"kind": "RULE_MATCH", "ruleIndex": 0, "ruleId": "r1", "inExperiment": true}
{"kind": "FALLTHROUGH", "inExperiment": true}
{"kind": "PREREQUISITE_FAILED", "prerequisiteKey": "other-flag"}
{"kind": "ERROR", "errorKind": "FLAG_NOT_FOUND | MALFORMED_FLAG | INVALID_CONTEXT | WRONG_TYPE"}
```

`inExperiment` is present only when true. OpenFeature providers map them to resolution reasons: `OFF` →
`DISABLED`; `TARGET_MATCH` and `RULE_MATCH` → `TARGETING_MATCH`; `FALLTHROUGH` → `SPLIT` when the fallthrough is a
rollout, else `DEFAULT`; `PREREQUISITE_FAILED` → `PREREQUISITE_FAILED`; `ERROR` → `ERROR` with the OpenFeature
error code (`FLAG_NOT_FOUND`, `PARSE_ERROR` for malformed flags, `INVALID_CONTEXT` or `TARGETING_KEY_MISSING`,
`TYPE_MISMATCH`). The OpenFeature variant is the variation index as a decimal string.

## OpenFeature contexts

An OpenFeature evaluation context maps to a flag context like this: `targetingKey` becomes `key` (unless `key` is
set); an attribute `kind` sets the kind; with `kind: "multi"`, each object-valued attribute is one kind's context,
whose `targetingKey` likewise becomes its `key`, and top-level attributes that aren't objects (including
`targetingKey`) are dropped. Everything else is an attribute. A missing key maps to the OpenFeature error
`TARGETING_KEY_MISSING`, any other invalid context to `INVALID_CONTEXT`.