Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/multivariate-evaluation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"@reflag/flag-evaluation": major
---

Introduce a v2-only compiled-variant evaluator:

- `newEvaluator({ flags, segments })` prepares a complete collection and returns `evaluateAll(context)` and `evaluateFlag(context, flagKey)` methods. The batch result contains shared normalized context and per-flag results without repeated context. Single-flag evaluation visits only the requested flag and its dependencies. Unresolved batch entries do not prevent other flags from being evaluated. Remove the v1 value-rule APIs and the one-shot `evaluateFlag` wrapper.
- Preserve flag and segment reference nodes instead of inlining their filters. Flag references explicitly match variant keys; segments remain reusable predicates. Resolve references recursively without dependency-result caching or memoization.
- Preserve diagnostics from unsuccessful referenced rules without suppressing a valid matching selection. Invalid nonmatches remain fail-closed through negation. Propagated diagnostics identify their source flag or segment.
- Reject duplicate keys, missing references, unknown referenced variant keys and cycles at preparation, including mixed flag/segment cycles. Evaluating an unknown flag key throws.
- Preserve arbitrary variant value types through `CompiledFlag<T>` and `EvaluationResult<T>` (default `any`). `resolved: true | false` distinguishes a selected value, including `undefined`, from failure. Successful results require the variant key and matched rule ID.
- Keep membership Sets and prepared percentage bounds private. `ANY_OF` / `NOT_ANY_OF` use cached Set lookups, including under groups and negations; percentage distributions use precomputed bounds and a simple scan.
- Evaluate rules sequentially with explicit fallthrough and source-version/rule/allocation diagnostics.
- Return structured `INVALID_COMPARISON` errors for invalid numeric/date comparisons instead of logging context values. Invalid conditions cannot match through negation.
- Retain the original hash function and threshold rollout filter for boolean cohort compatibility. The inclusive maximum distribution bucket selects the last nonzero allocation.

The runtime exports are `newEvaluator`, `flattenContext`, and `hashInt`. Removed APIs include `evaluateFlagRules`, `newFlagEvaluator`, `evaluateFlag`, the scalar `evaluate` helper, and the legacy `flattenJSON` / `unflattenJSON` helpers.
18 changes: 18 additions & 0 deletions .changeset/node-sdk-collection-values.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"@reflag/node-sdk": major
"@reflag/openfeature-node-provider": patch
---

Adopt evaluator v2 and the complete `GET /flags` collection protocol in the Node SDK.

- `getFlag(context, key)` and bound `getFlag(key)` are direct Boolean reads with an implicit `false` default. Positional defaults infer the value type, widening literals, without generated declarations. Non-null defaults require the same top-level JSON category; missing/mismatched values use the default without authored-selection metadata. An explicit `null` default accepts arbitrary JSON, including authored null. Nested payload schemas are not validated. There is no flag wrapper, `.isEnabled`, `.config`, or flag `.track()` method. Batch `getFlags()` keeps raw JSON/`undefined` values. Ordinary client `track()` APIs remain separate.
- Singular remote reads share the default contract. Root `getFlagRemote(key, defaultValue, userId?, companyId?, context?)` puts the default second, moving old positional IDs one place right; pass `undefined` for a Boolean read with IDs. Bound `getFlagRemote(key, defaultValue?)` needs no explicit IDs.
- Reads immediately register checks for requested roots. Checks carry the selected variant key as `evalResult`, `sourceVersionId`, and the snapshot `flagStateVersion`. Boolean values send the keys `"true"` / `"false"`. Equal-valued variants and dependency-only version changes retain their distinct identities. Delivery remains buffered and rate-limited.
- Prepare complete flags/segments snapshots before atomically accepting them. Evaluate references recursively without dependency-result caching.
- Persist full graphs and snapshot versions in version-2 fallback snapshots. V1 snapshots are not converted. Built-in storage uses separate versioned keys/files so V1 and V2 clients cannot overwrite one another's snapshots. Local overrides and value-only fallbacks do not fabricate authored variant selections.
- Bootstrap state contains v2 selection results and context/version metadata, without registering browser checks. Existing v1 browser bootstrap consumers require a separate migration; this is not a backwards-compatible bootstrap shape.
- Preserve sync modes, ordinary tracking, bound contexts, and post-evaluation override layering. Update the Express example to consume direct values.

The existing OpenFeature provider and v1 React bootstrap demo remain explicitly pinned to published Node SDK v1 through an npm alias. This prevents automatic workspace dependency upgrades from silently breaking their wrapper/bootstrap contracts; migrating those consumers to multivariate values is separate work.

This release requires the collection-capable evaluator and compatible `/flags` and variant-key check ingestion. It does not migrate legacy remote config into multivariate flag values.
117 changes: 117 additions & 0 deletions packages/flag-evaluation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Flag evaluation v2

Prepare a complete collection of compiled flags and segments when definitions
are loaded or refreshed, then evaluate all flags or select one flag for each context:

```ts
import { newEvaluator } from "@reflag/flag-evaluation";

const evaluator = newEvaluator({
flags: compiledFlags,
segments: compiledSegments,
});
const context = { company: { id: "acme" }, user: { id: "alice" } };

// getFlags() / getFlagsForBootstrap(): all flags, one normalized context.
const batch = evaluator.evaluateAll(context);
const selected = batch.flags["my-flag"];

// getFlag(): just the requested flag and its dependencies.
const result = evaluator.evaluateFlag(context, "my-flag");

// Diagnostics can accompany either a successful result or a failure.
if (result.errors.length) console.warn(result.errors);

if (result.resolved) {
// T, not T | undefined. An undefined variant value is still a success.
console.log(result.value);
// Required success metadata for diagnostics/exposure events.
console.log(result.variantKey, result.matchedRuleId);
} else {
// No variant was selected: use the SDK's fallback behavior.
}
```

`CompiledFlag<T = any>`, `CompiledCollection<T = any>`,
`FlagEvaluationResult<T = any>` and `EvaluationResult<T = any>` preserve the
caller's value type. A heterogeneous collection can use a union for `T`.
Non-fatal diagnostics may also accompany a successful default/fallthrough result.

`evaluateAll(context)` returns `CollectionEvaluationResult<T>`: a shared normalized
`context` and a `flags` record keyed by flag key. Each entry is a
`FlagEvaluationResult<T>` with its own resolution status, value, metadata and
errors, without a repeated `context`. Unresolved flags do not prevent other flags
from being evaluated; segments are not returned as outputs.

`evaluateFlag(context, flagKey)` returns `EvaluationResult<T>`, which adds normalized
`context` to one flag result. It evaluates dependencies but not unrelated flags.
Both operations use the complete collection for resolving references.

Rebuild the evaluator when definitions change. Treat definitions and returned
values as immutable. Each operation has independent diagnostics. SDK-specific
tracking, fallbacks, overrides and bootstrap snapshot versions remain outside the
evaluator.

## References

A collection contains `flags: CompiledFlag<T>[]` and optional
`segments: { key: string; filter: RuleFilter }[]`. Keep references in filters
instead of substituting the referenced definition's filters:

```ts
// Match the variant selected by another flag in the collection.
const flagReference = { type: "flag", key: "model", variantKey: "quality" };
// Reuse a segment predicate from the same collection.
const segmentReference = { type: "segment", key: "enterprise" };
// Negation and authored AND/OR groups work with either reference type.
const negativeReference = { type: "negation", filter: flagReference };
```

Flag references match **variant keys**, never the truthiness of variant values.
The referenced flag uses its own ordered rules, distributions and rollout salts.
A matching selection remains valid despite diagnostics from unsuccessful sibling
rules. A nonmatch accompanied by diagnostics is invalid, not a clean `false`, so
negating an uncertain nonmatch cannot grant access. A clean nonmatch can be
negated. This is intentionally more conservative than explicitly matching a
selected fallback variant.

Segments are predicates, not independent enable-rule groups: errors in a segment
invalidate the containing rule, including through negation. Diagnostics propagated
through references include `source: { type: "flag" | "segment", key: string }`
identifying their original definition. Existing short-circuit behavior is retained;
unvisited runtime branches do not produce diagnostics.

`newEvaluator` throws for duplicate keys, missing references, unknown referenced
variant keys, and cycles, including mixed flag/segment cycles and references in
unreachable branches. Evaluating an unknown flag key also throws. Flag and segment
keys occupy separate namespaces.

References are evaluated recursively without dependency-result caching or
memoization. Context is normalized once per call; repeated references are evaluated
again. Only the existing definition-level membership Sets and percentage bounds
are prepared ahead of time.

## Migration from v1

V2 exports `newEvaluator(collection)`, `flattenContext`, and `hashInt`, plus their
public types. `newEvaluator` accepts a complete compiled collection, not value
rules or a single flag. It returns an object with `evaluateAll(context)` and
`evaluateFlag(context, flagKey)` methods, rather than a callable evaluator.
Publishers and consumers must adopt the collection/reference protocol together;
the unchanged Node SDK remains on the published v1 evaluator.

V1's `evaluateFlagRules`, scalar `evaluate`, `flattenJSON`, `unflattenJSON`, and
value-rule types are removed. There is no v2 one-shot evaluation wrapper.
The hash and `rolloutPercentage` filter remain to preserve boolean cohorts.

## Performance checks

From the repository root:

```sh
yarn workspace @reflag/flag-evaluation exec vitest bench --run test/variants.bench.ts
```

Benchmarks exclude preparation and compare with published v1 across candidate
lists up to 100,000 and percentage distributions up to 50 allocations. Unit tests
assert cached Set usage, no repeated percentage reads, and stable hash cohorts.
4 changes: 3 additions & 1 deletion packages/flag-evaluation/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@reflag/flag-evaluation",
"version": "1.1.1",
"version": "2.0.0-next.0",
"license": "MIT",
"repository": {
"type": "git",
Expand All @@ -16,6 +16,7 @@
},
"scripts": {
"build": "tsc --project tsconfig.build.json",
"prepack": "yarn build",
"test": "vitest",
"test:ci": "vitest --reporter=default --reporter=junit --outputFile=junit.xml",
"lint": "oxlint .",
Expand All @@ -30,6 +31,7 @@
"js-sha256": "0.11.0"
},
"devDependencies": {
"@reflag/flag-evaluation-v1": "npm:@reflag/flag-evaluation@1.1.1",
"@reflag/tsconfig": "^0.0.2",
"@types/node": "^22.12.0",
"ts-node": "^10.9.2",
Expand Down
Loading
Loading