diff --git a/docs/modules/actuator.md b/docs/modules/actuator.md
index 32fc4d5d..1921de98 100644
--- a/docs/modules/actuator.md
+++ b/docs/modules/actuator.md
@@ -23,6 +23,9 @@ always-on, and secured entirely by M11 config with zero code edge to `firefly/se
**per-process** on the default `memory` authorizations driver — a map rebuilt in every PHP worker — which is
why the payload states its own storage model in `authorizations.processLocal`; `authorizations.driver =
eloquent` is what makes them describe the deployment
+- `/actuator/flags` (+ `/actuator/flags/{key}`, GET/POST) — supplied by [`firefly/feature-flags`](feature-flags.md)
+ when enabled: flags with origins and expiry, layers and history, previews, and writes when management writes
+ and a store are enabled
!!! tip "A browser view over all of this"
`firefly/admin` renders these same endpoints as a server-side dashboard, reading them **in-process** rather
diff --git a/docs/modules/admin.md b/docs/modules/admin.md
index 69ced98f..c124e40b 100644
--- a/docs/modules/admin.md
+++ b/docs/modules/admin.md
@@ -25,7 +25,7 @@ component requirement above.
Most pages are a view over one `ActuatorEndpoint`'s payload; four read the container instead. The menu groups
them the way an operator thinks rather than the way the packages are laid out — *what is it doing right now*,
-*what did it wire at boot*, *what is its data*, *how is it configured* — because a flat list of eighteen links
+*what did it wire at boot*, *what is its data*, *how is it configured* — because a flat list of nineteen links
is a worse menu than four short ones.
| Group | Page | Path | Endpoint | Answers |
@@ -44,11 +44,18 @@ is a worse menu than four short ones.
| Configuration | Config properties | `/firefly/configprops` | `configprops` | Every `#[ConfigProperties]` DTO the application bound, with the values it resolved |
| Configuration | Caches | `/firefly/caches` | `caches` | The cache stores this application has configured |
| Configuration | Loggers | `/firefly/loggers` | `loggers` | Log channels and their levels, with a control to change one |
+| Configuration | **Feature flags** | `/firefly/flags` | `flags` | Every feature flag, its defining layer and history, an evaluation preview, and controls to toggle, edit and delete when a flag store and `firefly.feature-flags.management.writes` are enabled |
| Configuration | **Feature switches** | `/firefly/settings` | — | Every framework switch, where its value came from, and — outside production — a control. **Off by default**; see [below](#the-feature-switch-console) |
| Data | **Datasource** | `/firefly/datasource` | — | Connections, connection reuse, and the compiled `#[Transactional]` contract |
| Data | **Browse data** | `/firefly/data` | — | The records behind your repositories — see [Data Browser](data-browser.md). **Off by default** |
| Data | **Entity map** | `/firefly/data-map` | — | The entities and the foreign keys between them, drawn |
+The Feature flags page shows the effective definition, each source layer and store change history. It offers
+controls only when a store is configured and `firefly.feature-flags.management.writes` is enabled. Its evaluation
+preview uses the context entered on the page and records no metric or exposure event. If a write is accepted but
+the new definition is not yet visible, the page asks you to check again after refresh; it does not report a
+deletion. Writes made by an authenticated principal retain that principal in history.
+
The four pages with no endpoint read the container rather than the actuator, and each decides its own visibility:
an entry that led to "there is nothing here" is worse than no entry.
@@ -469,7 +476,7 @@ container image, and a web form that edits the file holding your database passwo
- **No instance registry.** Spring Boot Admin is a separate server that many applications register *with*, giving
one console across a fleet. This is a per-instance dashboard, which is what makes the in-process read possible;
a fleet view would need a different design and is not planned.
-- **No write operations besides the log level, the data browser and the feature switches.** `/caches` is read-only
+- **No write operations besides the log level, the data browser, the feature switches and feature flags.** `/caches` is read-only
for the same reason it is read-only on the JSON surface — `firefly/actuator` carries no code edge to
`firefly/security` and so cannot say who asked.
- **The Health panel ignores `show-details` entirely**, reading the contributor registry rather than the JSON
diff --git a/docs/modules/feature-flags.md b/docs/modules/feature-flags.md
new file mode 100644
index 00000000..0601c888
--- /dev/null
+++ b/docs/modules/feature-flags.md
@@ -0,0 +1,561 @@
+# Feature Flags
+
+`firefly/feature-flags` is LaraFly's feature-flag subsystem, built on [OpenFeature](https://openfeature.dev): flags are
+[flagd](https://flagd.dev) flag definitions, evaluated in-process by a PHP port of flagd's evaluator, and an
+application gates code with an attribute, a route middleware, a Blade directive or a facade call. Operators change
+flags at runtime from the [admin dashboard](admin.md), the `flags` actuator endpoint or `php artisan firefly:flags`,
+with an audit trail, and every PHP-FPM worker sees the change within the source's refresh interval.
+
+The same flag document evaluates to the same value, variant and reason in LaraFly and in PyFly, and a user lands in
+the same percentage bucket in both: the two frameworks share [one contract](../feature-flags-contract.md) and run the
+same conformance files in their test suites.
+
+The checked-in [Lumen sample](https://github.com/fireflyframework/fireflyframework-php/tree/main/samples/lumen) exercises the facade, a gated route, shared targeting
+vectors, test overrides, store transactions and preview exposure behavior. Both editions of *LaraFly by Example*
+develop that sample in Chapter 13A.
+
+## Quick start
+
+Switch the subsystem on and declare flags in `config/firefly.php`:
+
+
+```php
+return [
+ 'feature-flags' => [
+ 'enabled' => true,
+ 'flags' => [
+ 'new-checkout' => false,
+ 'checkout-flow' => 'v2',
+ ],
+ ],
+];
+```
+
+`true` and `false` are shorthand for a boolean flag with the variants `on` and `off`; a string is shorthand for a
+flag with that single variant. Then ask for a flag wherever you need it:
+
+
+```php
+use Firefly\FeatureFlags\FeatureFlags;
+
+final class CheckoutPage
+{
+ public function __construct(private readonly FeatureFlags $flags) {}
+
+ public function template(): string
+ {
+ if (! $this->flags->isEnabled('new-checkout')) {
+ return 'checkout.legacy';
+ }
+
+ return $this->flags->getString('checkout-flow', 'v1') === 'v2' ? 'checkout.v2' : 'checkout.v1';
+ }
+}
+```
+
+`FeatureFlags` has a typed getter per flag type — `isEnabled()`, `getString()`, `getInt()`, `getFloat()`,
+`getObject()` — plus `variant()` and `details()`, which returns the value, the variant, the reason, the error code
+and the flag's metadata. Every getter takes the default to answer when the flag is missing or fails, and an optional
+evaluation context and targeting key. Under the facade sits a named OpenFeature client (`Client` is a bean too), so
+code written against the OpenFeature PHP SDK works unchanged.
+
+## Defining flags
+
+A flag is a flagd flag definition: a `state`, the `variants` it can answer, the `defaultVariant`, optional
+`targeting` rules in JSON Logic, and optional `metadata`.
+
+
+```json
+{
+ "flags": {
+ "new-checkout": {
+ "state": "ENABLED",
+ "variants": {"on": true, "off": false},
+ "defaultVariant": "off",
+ "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]},
+ "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}
+ }
+ },
+ "$evaluators": {"is-beta": {"in": ["beta", {"var": "roles"}]}}
+}
+```
+
+- **Keys** match `^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$`; they appear in URLs and CLI arguments.
+- **Variants** all hold one JSON type: boolean, string, number, or object.
+- **Targeting** returns a variant name, or `null` to fall back to `defaultVariant`. The operators are flagd's:
+ JSON Logic plus `fractional`, `sem_ver`, `starts_with` and `ends_with`; `{"$ref": "is-beta"}` reuses a shared rule
+ from `$evaluators`.
+- **Metadata** values are scalars. Four keys are reserved: `description`, `owner`, `kind` (`release`, `experiment`,
+ `ops` or `permission`) and `expires` (`YYYY-MM-DD`). A flag past its `expires` date still evaluates; it is flagged
+ as expired in the admin page, the actuator, the health details, and one warning per day in the log.
+- A field flagd does not define is kept as written and ignored.
+
+An invalid definition in `flags` or in a flag file refuses the boot, naming the key and the reason (for example
+`Invalid feature flag [bad key] from source [config]: invalid flag key.`); an invalid document from the sync
+endpoint or the store is rejected as a whole and the last good one kept.
+After a file has loaded successfully, a later invalid edit also leaves its last good document in force. Replace a
+file atomically (write a sibling file, then rename it) so a polling worker never parses a partial write. Quote YAML
+date-like keys and values: YAML parsers otherwise disagree about dates and non-string keys that JSON cannot express.
+
+## Targeting recipes
+
+Every recipe is a flag definition you can put in `flags`, a flag file or the store.
+
+**Release toggle and kill switch.** A boolean flag, off by default; turning it on (or a `DISABLED` state, which
+makes every gate answer its default) needs no deploy once the store is enabled.
+
+
+```json
+{"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off"}
+```
+
+**Progressive rollout.** `fractional` hashes the flag key and the targeting key (the signed-in principal's name),
+so a user stays in their bucket as the percentages move, in PHP and in Python alike. Anonymous traffic has no
+targeting key and gets the default variant.
+
+
+```json
+{"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off",
+ "targeting": {"fractional": [["on", 10], ["off", 90]]}}
+```
+
+**Beta cohort.** `roles` holds the principal's `ROLE_` authorities without the prefix.
+
+
+```json
+{"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off",
+ "targeting": {"if": [{"in": ["BETA", {"var": "roles"}]}, "on", null]}}
+```
+
+**Per-tenant or per-plan entitlement.** `tenant` comes from the principal attribute named by
+`firefly.feature-flags.context.tenant-attribute`; `plan` comes from a context contributor of your own (below).
+
+
+```json
+{"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off",
+ "targeting": {"if": [{"or": [{"in": [{"var": "tenant"}, ["acme", "globex"]]}, {"==": [{"var": "plan"}, "enterprise"]}]}, "on", null]}}
+```
+
+**Multivariate experiment.** A weighted split with a holdout; switch `firefly.feature-flags.events.evaluations` on to
+publish a `FeatureFlagEvaluated` event per evaluation — the exposure record the experiment's analysis needs.
+
+
+```json
+{"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control",
+ "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]},
+ "metadata": {"kind": "experiment", "owner": "growth"}}
+```
+
+## Gating code
+
+**On a bean.** `#[FeatureFlag]` on a method (or a class: every public method) of any stereotyped bean. While the
+flag is off, the call answers the `fallback` method — called with the same arguments — or throws
+`FeatureFlagDisabledException`. `variant:` gates on one variant; `default: true` opens the gate when the flag is
+missing.
+
+
+```php
+
+```php
+ 'Available balance'];
+ }
+}
+```
+
+Routes declared in route files use the same middleware by its alias, `feature-flag:{key}[,{variant}]`:
+
+
+A gated route that is off answers `firefly.feature-flags.web.disabled-status` — 404 (the default: a dark launch looks
+like a page that does not exist), 403 or 503 — as problem+json, and never names the flag.
+
+**In a template.**
+
+
+```blade
+@featureflag('new-checkout')
+ @include('checkout.v2')
+@else
+ @include('checkout.legacy')
+@endfeatureflag
+
+@featurevariant('checkout-flow', 'v2')
+
Try the faster checkout.
+@endfeaturevariant
+```
+
+Every gate fails **closed** on a missing flag, an evaluation failure, a type mismatch or a switched-off subsystem:
+it answers the gate's default, which is "off" unless the gate says `default: true`. A source failure after a
+successful load retains the last-good document, so a previously enabled gate can remain open during an outage.
+A flag never decides which beans exist:
+bean conditions are evaluated once at boot, so a boot-time switch is a property — use `#[ConditionalOnProperty]`.
+
+## Evaluation context
+
+The framework builds a context for every evaluation, and the caller's own attributes win over it:
+
+| Attribute | Value |
+|---|---|
+| `targetingKey` | the authenticated principal's name; absent when anonymous |
+| `roles` | the principal's `ROLE_` authorities, without the prefix |
+| `tenant` | the principal attribute named by `firefly.feature-flags.context.tenant-attribute` |
+| `application` | `app.name` |
+| `profiles` | the active Firefly profiles |
+
+Add your own attributes with a context contributor — any `#[Component]` implementing
+`EvaluationContextContributor`; `#[Order]` sets the order, and a later contributor may override an earlier one:
+
+
+```php
+use Firefly\Container\Attributes\Component;
+use Firefly\FeatureFlags\Context\EvaluationContextBuilder;
+use Firefly\FeatureFlags\Context\EvaluationContextContributor;
+
+#[Component]
+final class PlanContributor implements EvaluationContextContributor
+{
+ public function __construct(private readonly Subscriptions $subscriptions) {}
+
+ public function contribute(EvaluationContextBuilder $context): void
+ {
+ $customer = $context->targetingKey();
+ if ($customer !== null) {
+ $context->set('plan', $this->subscriptions->planOf($customer));
+ }
+ }
+}
+```
+
+An explicit context overlays all contributors. `targetingKey` in JSON context accepts a nonempty string or an
+integer converted to decimal text; a separate nonempty targeting-key argument wins over it. LaraFly refuses an
+Eloquent model, `Arrayable` or `Jsonable` object as a targeting key: pass the user's stable id. It drops unsupported
+attribute values and numeric-looking attribute names with a DEBUG diagnostic. A `DateTimeInterface` value becomes
+UTC epoch milliseconds, as in PyFly.
+
+## Sources and precedence
+
+Flags come from up to four sources, lowest precedence first: **config** (`flags` and `evaluators`) → a watched
+**file** → another service's **http** sync endpoint → the writable **store**. The highest source that defines a key
+supplies its whole definition; the admin page and the actuator show which source won (`origin`) and which it
+shadows (`overrides`).
+
+
+```php
+return [
+ 'feature-flags' => [
+ 'enabled' => true,
+ 'sources' => [
+ 'file' => ['enabled' => true, 'path' => base_path('flags.yaml'), 'refresh-interval' => '5s'],
+ 'http' => ['enabled' => true, 'url' => 'https://control-plane.internal/feature-flags/flagd.json', 'token' => env('FLAGS_TOKEN'), 'refresh-interval' => '30s', 'timeout' => '2s'],
+ 'store' => ['enabled' => true, 'driver' => 'database', 'connection' => null, 'refresh-interval' => '5s'],
+ ],
+ ],
+];
+```
+
+PHP-FPM shares nothing between requests, so the registry keeps each source's last good document and its revision
+in the application cache. A request re-checks a source only when its `refresh-interval` has elapsed — a file's
+modification time and size, the store's newest change id, an HTTP conditional GET under a non-blocking lock (one
+worker fetches; the others serve the last good document meanwhile) — and the request that sees a change publishes
+`FeatureFlagsChanged`. A long-lived process (Octane, a queue worker) re-checks on the same schedule. When the cache
+store is down, sources are read in-process and flags keep evaluating.
+
+Give each application its own application-cache prefix and retain source entries without eviction; a fresh worker
+needs those entries to recover the last good file or remote document. A file revision uses whole-second mtime and
+size: same-size edits within the same timestamp can remain unseen indefinitely, even after atomic rename. Publish
+atomically **and** advance the file's mtime to a distinct second (or change its size); do not rely on rename alone.
+
+Change events are best effort, not durable or exactly once. Rolling workers with different configuration can
+alternate shared change announcements. Cache outages suspend shared announcements; recovery with no composed
+record reports `startup`. Use the store audit history when you need a durable record of operator writes.
+
+## The store
+
+The store is the writable layer behind the admin page, the actuator endpoint and the CLI. A write replaces the
+key's definition for every lower source until it is deleted; every write also appends a change row (who, what,
+when), and a write may carry `expectedVersion` so two operators cannot overwrite each other.
+The relational tables keep exact-case keys, JSON payloads and zoneless UTC microsecond timestamps across SQLite,
+PostgreSQL, MySQL and MariaDB. A write inside the caller's transaction remains pending until that caller commits;
+rollback removes both the definition and audit row. If MariaDB aborts an existing snapshot after a competing insert,
+the caller must roll back and retry its complete operation. A post-commit local refresh failure preserves the
+committed write and the last good local composition.
+
+With the `database` driver, publish and run the migration:
+
+
+```bash
+php artisan vendor:publish --tag=firefly-feature-flags-migrations
+php artisan migrate
+```
+
+The migration is also loaded automatically while the store is enabled with the `database` driver. It creates
+`firefly_feature_flags` and `firefly_feature_flag_changes`, the same tables PyFly creates: two services that share
+a database share their flags, and either framework reads the rows the other wrote. `connection` picks the Laravel
+connection (null is the default one). The `memory` driver keeps flags per process, for tests and demos.
+
+## Managing flags
+
+**Actuator.** `GET /actuator/flags` lists the provider, the sources and every flag; `GET /actuator/flags/{key}`
+describes one, with each source's definition and the latest 50 changes; `POST /actuator/flags/{key}` evaluates
+(`{"action": "evaluate", "context": {…}, "targetingKey": "…"}`) or writes (`enable`, `disable`,
+`default-variant`, `put`, `delete`). The answers are the same JSON PyFly answers, and a refusal is
+`{"error": code, "message": text}` with a status per code: `writes-disabled` 403, `not-writable` 409,
+`invalid-definition` 422, `unknown-flag` 404, `unknown-variant` 422, `conflict` 409, `bad-request` 400.
+
+The sensitive `/actuator/flags` endpoint answers 404 until `flags` is named in `firefly.management.endpoints.web.exposure.include`, and
+writes additionally need `firefly.feature-flags.management.writes` and a store:
+
+
+```php
+return [
+ 'management' => [
+ 'endpoints' => ['web' => ['exposure' => ['include' => 'health,info,flags']]],
+ ],
+ 'feature-flags' => [
+ 'enabled' => true,
+ 'sources' => ['store' => ['enabled' => true]],
+ 'management' => ['writes' => true],
+ ],
+];
+```
+
+
+```bash
+curl -s http://localhost:8000/actuator/flags
+curl -s -X POST http://localhost:8000/actuator/flags/new-checkout -H 'Content-Type: application/json' -d '{"action":"enable"}'
+curl -s -X POST http://localhost:8000/actuator/flags/new-checkout -H 'Content-Type: application/json' -d '{"action":"evaluate","targetingKey":"ada"}'
+```
+
+The actor recorded for a write is the authenticated principal's name, or `actuator` when there is none. An
+`evaluate` is a preview: it uses the context you send plus the application's own attributes — never yours — and
+counts in no metric and no exposure record.
+After a successful `put`, `{"key":"…","refreshPending":true}` means no new local definition is visible yet;
+it does not claim a caller-owned transaction has committed. If an older last-good definition remains visible, the
+response describes that current view. Poll the detail endpoint or wait for the source refresh before assuming the
+local process has adopted the new revision.
+
+**Admin.** The [admin dashboard](admin.md)'s **Feature flags** page lists every flag with its state, origin and
+expiry, toggles one, edits its definition as JSON, sets its default variant, previews an evaluation and shows its
+history. Its writes follow the same rules and record the actor `admin` when nobody is signed in.
+
+**CLI.**
+
+
+```bash
+php artisan firefly:flags list
+php artisan firefly:flags show new-checkout --json
+php artisan firefly:flags evaluate checkout-flow --context='{"plan":"pro"}' --targeting-key=ada
+php artisan firefly:flags enable new-checkout --expected-version=0
+php artisan firefly:flags disable new-checkout --expected-version=1
+php artisan firefly:flags default-variant checkout-flow v2
+php artisan firefly:flags put banner --file=banner.json
+php artisan firefly:flags delete banner
+```
+
+A command-line write records `cli:
`.
+For `evaluate`, omit `--context` or pass `--context='{}'` when there are no explicit attributes. If supplied,
+`--context` must contain a JSON object; empty text, JSON null, arrays, scalars and malformed JSON fail with
+`bad-request` and exit 1 before any evaluation. Add `--json` to receive the portable error body.
+
+## The sync server
+
+One service can serve its effective flag set to others — a control plane serving every service of a product, in
+either framework. The server answers `GET` on its path with the composed flagd document, an `ETag`, and `304` for a
+matching `If-None-Match`; it requires `Authorization: Bearer `, and enabling it without a token refuses the
+boot unless `allow-anonymous` is set.
+
+
+```php
+return [
+ 'feature-flags' => [
+ 'enabled' => true,
+ 'server' => ['enabled' => true, 'path' => '/feature-flags/flagd.json', 'token' => env('FLAGS_SERVER_TOKEN')],
+ ],
+];
+```
+
+A consumer points its `http` source at that URL with the same token (see [Sources](#sources-and-precedence)). Test
+overrides are never served.
+
+## Observability
+
+- **Metric:** `feature_flag_evaluations_total{flag, variant, reason}` on the meter registry when
+ [Observability](observability.md) is installed and `firefly.observability.metrics.enabled` is on (the default).
+ When metrics are disabled or no meter is available, feature flags use `NoOpFeatureFlagMetrics`; evaluation still
+ works but no counter is recorded. `variant` is `none` when there is none, and a failed evaluation counts with
+ `reason="ERROR"` when the meter is active.
+- **Health:** the `featureflags` component is UP while every source has loaded at least once, DOWN while one never
+ has, and lists each source's status, the flag count and the expired flags.
+- **Events:** `FeatureFlagsChanged(changedKeys, origin)` when the effective set changes, `FeatureFlagUpdated` after a
+ committed store write, and — with `firefly.feature-flags.events.evaluations` — `FeatureFlagEvaluated` per
+ evaluation.
+
+
+```php
+use Firefly\FeatureFlags\Event\FeatureFlagEvaluated;
+use Illuminate\Support\Facades\Event;
+
+Event::listen(FeatureFlagEvaluated::class, function (FeatureFlagEvaluated $exposure): void {
+ Exposures::record($exposure->key, $exposure->variant, $exposure->targetingKey);
+});
+```
+
+Telemetry never changes an evaluation: a failing meter or listener is logged at debug and the flag's value stands.
+When an exposure value contains more than 10,000 value occurrences, the exposure event is omitted, while the
+evaluation result and metric remain. The budget counts the root and every nested container or scalar, including
+repeated references; a cycle also exhausts the budget.
+
+An application may install a different OpenFeature provider for evaluation. Firefly's registry, source history,
+writable store and management operations then have no Firefly document to operate on; keep operator expectations
+limited to that provider's own capabilities. The PHP SDK does not expose Firefly's flag metadata, so use
+`FeatureFlags::details()` when metadata matters.
+
+## Testing
+
+`withFeatureFlags()` overrides flags of the running application for the rest of a test, above every source,
+shorthand included; see [Testing](testing.md#feature-flag-overrides).
+
+
+```php
+it('shows the faster checkout to the treatment group', function (): void {
+ withFeatureFlags(['new-checkout' => true, 'checkout-flow' => 'v2']);
+
+ $this->get('/checkout')->assertSee('Try the faster checkout.');
+});
+```
+
+## Configuration
+
+Every key, as the configuration reference documents it:
+
+
+```php
+'feature-flags' => [
+ 'enabled' => env('FIREFLY_FEATURE_FLAGS_ENABLED', false),
+
+ // 'flags' => [
+ // 'new-checkout' => false,
+ // 'checkout-flow' => [
+ // 'state' => 'ENABLED',
+ // 'variants' => ['control' => 'v1', 'treatment' => 'v2'],
+ // 'defaultVariant' => 'control',
+ // 'targeting' => ['fractional' => [['control', 50], ['treatment', 50]]],
+ // 'metadata' => ['owner' => 'payments', 'kind' => 'experiment', 'expires' => '2099-12-31'],
+ // ],
+ // ],
+ // 'evaluators' => [
+ // 'is-beta' => ['in' => ['beta', ['var' => 'roles']]],
+ // ],
+
+ // …
+ // 'sources' => [
+ // 'file' => ['enabled' => false, 'path' => '', 'refresh-interval' => '5s'],
+ // 'http' => ['enabled' => false, 'url' => '', 'token' => env('FIREFLY_FEATURE_FLAGS_HTTP_TOKEN', ''), 'refresh-interval' => '30s', 'timeout' => '2s'],
+ // 'store' => ['enabled' => false, 'driver' => 'database', 'connection' => null, 'refresh-interval' => '5s'],
+ // ],
+
+ // The principal attribute that holds the tenant id (the `tenant` evaluation-context attribute).
+ // 'context' => ['tenant-attribute' => 'tenant'],
+
+ // Status of a #[FeatureFlag]-gated route or method when the flag is off: 404, 403 or 503.
+ // 'web' => ['disabled-status' => 404],
+
+ // Publish a FeatureFlagEvaluated event per evaluation (exposure records for experiments).
+ // 'events' => ['evaluations' => false],
+
+ // Allow writes from the admin page, POST /actuator/flags/{key} and php artisan firefly:flags.
+ // 'management' => ['writes' => false],
+
+ // Serve this application's effective flag set to other services (bearer token required unless
+ // allow-anonymous is true; enabling it without a token refuses the boot).
+ // 'server' => ['enabled' => false, 'path' => '/feature-flags/flagd.json', 'token' => env('FIREFLY_FEATURE_FLAGS_SERVER_TOKEN', ''), 'allow-anonymous' => false],
+],
+```
+
+| Key (under `firefly.feature-flags`) | Default | Meaning |
+|---|---|---|
+| `enabled` | `false` | the master switch |
+| `flags` | `[]` | inline definitions, flagd or shorthand |
+| `evaluators` | `[]` | shared `$evaluators` rules |
+| `sources.file.enabled` / `.path` / `.refresh-interval` | `false` / `''` / `5s` | a watched flagd document (`.json`, `.yaml`, `.yml`) |
+| `sources.http.enabled` / `.url` / `.token` / `.refresh-interval` / `.timeout` | `false` / `''` / `''` / `30s` / `2s` | the sync client |
+| `sources.store.enabled` / `.driver` / `.connection` / `.refresh-interval` | `false` / `database` / `null` / `5s` | the writable layer |
+| `context.tenant-attribute` | `tenant` | the principal attribute holding the tenant id |
+| `web.disabled-status` | `404` | the status of a gated route or method that is off: 404, 403 or 503 |
+| `events.evaluations` | `false` | publish `FeatureFlagEvaluated` |
+| `management.writes` | `false` | allow writes from the admin page, the actuator and the CLI |
+| `server.enabled` / `.path` / `.token` / `.allow-anonymous` | `false` / `/feature-flags/flagd.json` / `''` / `false` | the sync server |
+
+## Troubleshooting
+
+- **Every gate is closed.** `firefly.feature-flags.enabled` is off, or the key is misspelt: gates fail closed. The
+ actuator's `GET /actuator/flags/{key}` answers `unknown-flag` for a key no source defines.
+- **A change does not show.** Each source is re-checked once per `refresh-interval`; the writing process sees its
+ own write when its local refresh succeeds. If that refresh is deferred or fails, the committed write remains
+ durable and normal polling restores local visibility. A store write made with the cache store down is seen by
+ other workers when their own interval elapses.
+- **The boot fails with `Invalid feature flag [...]`.** A definition in `flags` or in the flag file breaks a rule
+ above; the message names the key, the source and the rule.
+- **The boot rejects a source or sync server setting.** An enabled file source needs a path. An enabled HTTP source
+ needs a URL and a positive `refresh-interval`; zero is allowed for the cheaper file and store checks. An enabled
+ sync server needs a token unless `allow-anonymous` is true, and its path cannot be empty or `/`.
+- **Sync returns 401/403 before the controller.** The sync controller's bearer token does not bypass application
+ Security HTTP/JWT filters. Configure the sync path explicitly in the application's security rules so its intended
+ machine client can reach the controller, and retain the controller's own token check. Inspect both filter and
+ controller responses before changing credentials.
+- **`writes-disabled` / `not-writable`.** Set `firefly.feature-flags.management.writes`, and enable the store.
+- **A percentage rollout ignores anonymous users.** They have no targeting key; contribute a stable anonymous id
+ as `targetingKey` from a context contributor, or target on another attribute.
+- **`#[FeatureFlag]` refuses the boot.** The scanner names the method and the reason: remove `final`, move the
+ attribute to a public instance method, or name a fallback that exists.
diff --git a/docs/modules/testing.md b/docs/modules/testing.md
index 8ba6d2e1..6441f08d 100644
--- a/docs/modules/testing.md
+++ b/docs/modules/testing.md
@@ -156,6 +156,22 @@ between requests, so `actingAsPrincipal()` — a principal in the holder for one
login redirect, exactly as an anonymous browser is. The token, introspection, revocation and userinfo helpers are
machine endpoints and need no sign-in.
+## Feature flag overrides
+
+`withFeatureFlags(array $flags)` from `Firefly\Testing\functions.php` overlays definitions in the running
+application for the rest of the test. It accepts shorthand or full flagd definitions, stays in this process,
+and is neither stored nor served by the sync endpoint. The returned `FeatureFlagOverrides` supports `set()`,
+`forget()`, `merge()` and `clear()`. Enable `firefly.feature-flags.enabled` in the test configuration first.
+
+
+```php
+it('hides the beta report while its flag is off', function (): void {
+ withFeatureFlags(['beta-reports' => false]);
+
+ $this->get('/reports/beta')->assertNotFound();
+});
+```
+
## Pest expectations
Registered once, at monorepo boot, by `Firefly\Testing\Pest\FireflyExpectations::register()` — called from the
diff --git a/docs/publishing.md b/docs/publishing.md
index 580b84f6..8175d246 100644
--- a/docs/publishing.md
+++ b/docs/publishing.md
@@ -7,7 +7,7 @@ cross-repository `ACCESS_TOKEN` are needed.
## Package identity and compatibility
-The root package replaces all 29 former component names, including `firefly/firefly`, with
+The root package replaces all 30 former component names, including `firefly/firefly`, with
`self.version`. Packagist's `firefly` vendor namespace belongs to another publisher, so the public
package uses the Firefly Framework organization's namespace. `firefly/lumen` is a sample, not a replacement;
`firefly/skeleton` is a bundled project template, not a second published package.
@@ -81,7 +81,7 @@ CI runs these package checks on PHP 8.3, 8.4 and 8.5 for PRs to `main` and pushe
To repeat the public install check locally:
```bash
-RELEASE_TAG=v26.09.11 RELEASE_SHA="$(git rev-parse 'v26.09.11^{commit}')" php scripts/check-package-install.php --published
+RELEASE_TAG=v26.10.1 RELEASE_SHA="$(git rev-parse 'v26.10.1^{commit}')" php scripts/check-package-install.php --published
```
An unmerged branch or a local consumer check is not a published release.
diff --git a/docs/versioning.md b/docs/versioning.md
index 97a989eb..1c501117 100644
--- a/docs/versioning.md
+++ b/docs/versioning.md
@@ -21,14 +21,14 @@ The single place the current version *is* asserted in code is:
```php
final class Version
{
- public const string VERSION = '26.09.11';
+ public const string VERSION = '26.10.1';
}
```
-`Firefly\Kernel\Version::VERSION` has exactly two readers in the shipped packages, and `grep -rn
-'Version::VERSION' packages` is the whole list: `AboutCommand`, which prints `LaraFly ` as the first
-line of `php artisan firefly:about`, and `RuntimeInfoContributor`, which puts it at
-`runtime.firefly.version` in `/actuator/info`.
+`Firefly\Kernel\Version::VERSION` is the shipped runtime version source. `AboutCommand` prints it in
+`php artisan firefly:about`, `RuntimeInfoContributor` exposes it at `runtime.firefly.version` in `/actuator/info`,
+the installer's `NewCommand` uses it when creating applications, and `FeatureFlags::client()` supplies it to
+OpenFeature client metadata.
Consistency across the three human-visible surfaces that *should* always agree with it — the
`Version::VERSION` constant, the CHANGELOG's latest `## [x.y.z]` heading, and the README version badge — is
diff --git a/mkdocs.yml b/mkdocs.yml
index 8a3b88d0..57205475 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -155,6 +155,7 @@ nav:
- Admin Dashboard: modules/admin.md
- Bean Graph: modules/bean-graph.md
- Data Browser: modules/data-browser.md
+ - Feature Flags: modules/feature-flags.md
- Testing:
- Testing: modules/testing.md
- Integration Testing: modules/integration-testing.md
@@ -162,6 +163,7 @@ nav:
- Installer: modules/installer.md
- Reference:
- CLI Reference: cli.md
+ - Feature Flag Contract: feature-flags-contract.md
- Versioning: versioning.md
- Contributing:
- Contributing: contributing.md
diff --git a/packages/actuator/src/Endpoint/EndpointRequest.php b/packages/actuator/src/Endpoint/EndpointRequest.php
index ffa614c8..93f43d50 100644
--- a/packages/actuator/src/Endpoint/EndpointRequest.php
+++ b/packages/actuator/src/Endpoint/EndpointRequest.php
@@ -8,6 +8,8 @@
* The request an ActuatorEndpoint receives — the HTTP method, the path segments AFTER the endpoint id (e.g.
* /actuator/health/liveness → subPath ['liveness']), the parsed query, and the parsed JSON/form body. Built by
* ActuatorDispatchAction from the illuminate Request; scalar-only so endpoints stay trivially testable.
+ * The optional raw JSON body preserves object and number shapes lost by PHP's parsed array.
+ * The origin is trusted in-process metadata; HTTP dispatch never derives it from request input.
*/
final readonly class EndpointRequest
{
@@ -21,5 +23,7 @@ public function __construct(
public array $subPath,
public array $query = [],
public array $body = [],
+ public ?string $rawBody = null,
+ public string $origin = 'actuator',
) {}
}
diff --git a/packages/actuator/src/Web/ActuatorDispatchAction.php b/packages/actuator/src/Web/ActuatorDispatchAction.php
index f588d8b4..7d8f2d2b 100644
--- a/packages/actuator/src/Web/ActuatorDispatchAction.php
+++ b/packages/actuator/src/Web/ActuatorDispatchAction.php
@@ -60,7 +60,7 @@ public function __invoke(Request $request, string $path): Response
/** @var array $body */
$body = $request->isJson() ? (array) $request->json()->all() : $request->request->all();
- $result = $endpoint->handle(new EndpointRequest($request->getMethod(), $subPath, $query, $body));
+ $result = $endpoint->handle(new EndpointRequest($request->getMethod(), $subPath, $query, $body, $request->isJson() ? $request->getContent() : null));
if ($result === null) {
return $this->problems->render($this->notFound(), $request);
}
diff --git a/packages/actuator/tests/Support/RawBodyCapstoneTestCase.php b/packages/actuator/tests/Support/RawBodyCapstoneTestCase.php
new file mode 100644
index 00000000..d01ef8e7
--- /dev/null
+++ b/packages/actuator/tests/Support/RawBodyCapstoneTestCase.php
@@ -0,0 +1,14 @@
+ */
+ public array $seen = [];
+
+ public function endpointId(): string
+ {
+ return 'capture';
+ }
+
+ public function enabled(): bool
+ {
+ return true;
+ }
+
+ public function handle(EndpointRequest $request): EndpointResponse
+ {
+ $this->seen[] = $request;
+
+ return EndpointResponse::json(['ok' => true]);
+ }
+ };
+ /** @var ActuatorRegistry $registry */
+ $registry = $this->app()->make(ActuatorRegistry::class);
+ $registry->register($endpoint);
+
+ $this->call('POST', '/actuator/capture', [], [], [], ['CONTENT_TYPE' => 'application/json', 'HTTP_ACCEPT' => 'application/json'], '{"variants":{"none":{}}}')->assertOk();
+ $this->post('/actuator/capture', ['level' => 'debug'])->assertOk();
+
+ expect($endpoint->seen[0]->rawBody)->toBe('{"variants":{"none":{}}}')
+ ->and($endpoint->seen[0]->body)->toBe(['variants' => ['none' => []]])
+ ->and($endpoint->seen[1]->rawBody)->toBeNull()
+ ->and(new EndpointRequest('GET', []))->toHaveProperty('rawBody', null);
+});
diff --git a/packages/admin/resources/views/flags.blade.php b/packages/admin/resources/views/flags.blade.php
new file mode 100644
index 00000000..e76ffc9b
--- /dev/null
+++ b/packages/admin/resources/views/flags.blade.php
@@ -0,0 +1,211 @@
+@extends('firefly-admin::layout')
+@section('title', 'Feature flags')
+@section('body')
+ @php
+ $sources = is_array($overview['sources'] ?? null) ? $overview['sources'] : [];
+ $provider = is_array($overview['provider'] ?? null) ? $overview['provider'] : [];
+ @endphp
+
+
+
Feature flags
+
Inspect each flag, the layer that defines it, and its change history. Changes written to the flag store
+ become visible to other processes on their next refresh.
+
+
+ @if (session('data-message'))
+ {{ session('data-message') }}
+ @endif
+
+ @if ($unavailable ?? false)
+ Feature flag data is unavailable. Retry after the flags endpoint recovers.
+ @elseif (! ($overview['writable'] ?? false))
+ Read-only: no flag store is configured. Enable firefly.feature-flags.sources.store to change flags at runtime.
+ @elseif (! ($overview['writesEnabled'] ?? false))
+ Read-only: enable firefly.feature-flags.management.writes to change flags.
+ @endif
+
+ @if ($unavailable ?? false)
+ @elseif (isset($rawDetail))
+
+ @include('firefly-admin::_panel-head', ['title' => 'Complete flag detail JSON', 'count' => ''])
+
These JSON member names require the complete response to preserve their shape. Use the CLI or actuator to edit this definition.
+
{{ $rawDetail }}
+
+ @elseif (isset($missing))
+ @include('firefly-admin::_empty', ['title' => 'No such flag', 'body' => 'No layer defines '.e($missing).'. Back to every flag .'])
+ @elseif ($detail === null)
+
+ @include('firefly-admin::_panel-head', ['title' => 'Flags', 'count' => count($flags), 'filter' => 'flags-body', 'placeholder' => 'Filter flags…'])
+ @if ($flags === [])
+ @include('firefly-admin::_empty', ['title' => 'No flags defined', 'body' => 'Define a flag in configuration, a flag file, or the store.'])
+ @else
+
+ Key State Type Default From Version
+
+ @foreach ($flags as $flag)
+
+ {{ $flag['key'] ?? '' }}
+ @if ($flag['expired'] ?? false)expired @endif
+
+ {{ $flag['state'] ?? '' }}
+ {{ $flag['type'] ?? '' }}{{ ($flag['targeting'] ?? false) ? ' · targeted' : '' }}
+ {{ $flag['defaultVariant'] ?? '—' }}
+ {{ $flag['origin'] ?? '' }}
+ {{ $flag['version'] ?? '—' }}
+
+ @if ($writable)
+ @php $enabled = ($flag['state'] ?? '') === 'ENABLED'; @endphp
+
+ @endif
+
+
+ @endforeach
+
+
+ @endif
+
+
+
+ @include('firefly-admin::_panel-head', ['title' => 'Sources', 'count' => count($sources)])
+
+ Source Status Flags Last refresh Error
+
+ @foreach ($sources as $source)
+
+ {{ $source['name'] ?? '' }}
+ {{ $source['status'] ?? '' }}
+ {{ $source['flags'] ?? 0 }}
+ {{ $source['lastRefresh'] ?? '—' }}
+ {{ $source['error'] ?? '' }}
+
+ @endforeach
+
+
+
Provider {{ $provider['name'] ?? '' }}, {{ $provider['status'] ?? '' }}.
+
+ @else
+ @php
+ $key = (string) ($detail['key'] ?? '');
+ $definition = is_array($detail['definition'] ?? null) ? $detail['definition'] : [];
+ $history = is_array($detail['history'] ?? null) ? $detail['history'] : [];
+ $version = $detail['version'] ?? null;
+ @endphp
+
+ ← Every flag
+
+ @include('firefly-admin::_panel-head', ['title' => $key, 'count' => ($definition['state'] ?? '').' · from '.($detail['origin'] ?? '')])
+ @if ($detail['expired'] ?? false)
+
Expired. Its expires date has passed. Remove the flag or update the date.
+ @endif
+ @if ($writable)
+
+
+ @if ($version !== null)
+
+ @endif
+ @endif
+
+
+
+
+
+ @include('firefly-admin::_panel-head', ['title' => 'Evaluation preview', 'count' => ''])
+
+
+
+ Context (JSON object)
+ {{ $context }}
+ Targeting key
+
+ Evaluate
+
+ @if ($evaluation !== null)
+ @if ($evaluation['ok'])
+
+ {{ $evaluation['valueLabel'] }} {{ $evaluation['valueJson'] }}
+ Variant {{ $evaluation['body']['variant'] ?? '—' }}
+ Reason {{ $evaluation['body']['reason'] ?? '' }}
+ Error {{ $evaluation['body']['errorCode'] ?? '—' }}
+
+ @else
+
{{ $evaluation['error'] }}
+ @endif
+ @endif
+
The preview uses only this context and the application's attributes. It records no metric or exposure event.
+
+
+
+ @include('firefly-admin::_panel-head', ['title' => 'Layers', 'count' => count($layerRows)])
+
+ Source Definition
+
+ @foreach ($layerRows as $layer)
+ {{ $layer['source'] }} {{ $layer['definition'] }}
+ @endforeach
+
+
+
+
+
+ @include('firefly-admin::_panel-head', ['title' => 'History', 'count' => count($history)])
+ @if ($history === [])
+ @include('firefly-admin::_empty', ['title' => 'No changes recorded', 'body' => 'Only writes to the flag store are recorded; this flag has none.'])
+ @else
+
+ # Action Actor When (UTC)
+
+ @foreach ($history as $change)
+
+ {{ $change['id'] ?? '' }}
+ {{ $change['action'] ?? '' }}
+ {{ $change['actor'] ?? '—' }}
+ {{ $change['changedAt'] ?? '' }}
+
+ @endforeach
+
+
+ @endif
+
+ @endif
+@endsection
diff --git a/packages/admin/src/AdminEndpointReader.php b/packages/admin/src/AdminEndpointReader.php
index 9640ad71..b0701f36 100644
--- a/packages/admin/src/AdminEndpointReader.php
+++ b/packages/admin/src/AdminEndpointReader.php
@@ -6,6 +6,7 @@
use Firefly\Actuator\Endpoint\ActuatorRegistry;
use Firefly\Actuator\Endpoint\EndpointRequest;
+use Firefly\Actuator\Endpoint\EndpointResponse;
use Firefly\Actuator\Health\HealthContributorRegistry;
use Firefly\Config\Config;
use Illuminate\Contracts\Container\Container;
@@ -151,4 +152,23 @@ public function write(string $id, array $subPath, array $body): bool
return $response !== null && $response->status >= 200 && $response->status < 300;
}
+
+ /**
+ * @param list $subPath
+ * @param array $query
+ * @param array $body
+ */
+ public function call(string $method, string $id, array $subPath = [], array $query = [], array $body = [], ?string $rawBody = null): ?EndpointResponse
+ {
+ $endpoint = $this->registry->get($id);
+ if ($endpoint === null || ! $this->has($id)) {
+ return null;
+ }
+
+ try {
+ return $endpoint->handle(new EndpointRequest($method, $subPath, $query, $body, $rawBody, origin: 'admin'));
+ } catch (Throwable) {
+ return null;
+ }
+ }
}
diff --git a/packages/admin/src/Web/AdminAction.php b/packages/admin/src/Web/AdminAction.php
index 5ecb8d84..b6621a35 100644
--- a/packages/admin/src/Web/AdminAction.php
+++ b/packages/admin/src/Web/AdminAction.php
@@ -40,6 +40,7 @@
use Illuminate\Http\Response;
use Illuminate\Routing\Router;
use Illuminate\Session\Store;
+use stdClass;
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
use Throwable;
@@ -112,6 +113,10 @@ public function __invoke(Request $request, string $page = ''): SymfonyResponse
return $this->datasourcePage($request, $current);
}
+ if ($slug === 'flags') {
+ return $request->isMethod('POST') ? $this->flagsWrite($request) : $this->flagsPage($request, $current);
+ }
+
if ($slug === 'settings') {
return $request->isMethod('POST') ? $this->settingsWrite($request) : $this->settingsPage($current);
}
@@ -252,6 +257,192 @@ private function settingsWrite(Request $request): SymfonyResponse
return $this->redirect($back, $this->console->set($key, $request->input('value') === '1'));
}
+ private function flagsPage(Request $request, AdminPage $current): SymfonyResponse
+ {
+ $overview = $this->flagsOverview();
+ if ($overview === null) {
+ return $this->flagsUnavailable($current);
+ }
+ $writable = ($overview['writable'] ?? false) === true && ($overview['writesEnabled'] ?? false) === true;
+ $key = $request->query('flag');
+
+ if (! is_string($key) || $key === '') {
+ return $this->html($this->render('flags', [
+ 'overview' => $overview,
+ 'flags' => is_array($overview['flags'] ?? null) ? array_values($overview['flags']) : [],
+ 'writable' => $writable,
+ 'detail' => null,
+ ], $current), 200);
+ }
+
+ $response = $this->reader->call('GET', 'flags', [$key]);
+ if ($response !== null && $response->status === 404) {
+ return $this->html($this->render('flags', [
+ 'overview' => $overview, 'flags' => [], 'writable' => $writable, 'detail' => null, 'missing' => $key,
+ ], $current), 404);
+ }
+
+ if ($response === null || $response->status !== 200 || ! is_string($response->body)) {
+ return $this->flagsUnavailable($current);
+ }
+ $detail = json_decode($response->body, true);
+ if (! is_array($detail) || ! is_string($detail['key'] ?? null) || ! is_array($detail['definition'] ?? null)
+ || ! is_array($detail['layers'] ?? null) || ! is_array($detail['history'] ?? null)) {
+ return $this->flagsUnavailable($current);
+ }
+ $objects = json_decode($response->body);
+ if (! $objects instanceof stdClass) {
+ return $this->html($this->render('flags', [
+ 'overview' => $overview, 'flags' => [], 'writable' => false, 'detail' => null,
+ 'rawDetail' => $response->body,
+ ], $current), 200);
+ }
+ $definition = property_exists($objects, 'definition') ? $objects->definition : new stdClass;
+ $variants = $definition instanceof stdClass && property_exists($definition, 'variants') && $definition->variants instanceof stdClass
+ ? array_map(static fn (int|string $name): string => (string) $name, array_keys(get_object_vars($definition->variants)))
+ : [];
+
+ $layers = [];
+ if (property_exists($objects, 'layers') && is_array($objects->layers)) {
+ foreach ($objects->layers as $layer) {
+ if ($layer instanceof stdClass) {
+ $layers[] = [
+ 'source' => is_string($layer->source ?? null) ? $layer->source : '',
+ 'definition' => (string) json_encode($layer->definition ?? null, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION),
+ ];
+ }
+ }
+ }
+
+ $context = $request->query('context');
+ $targetingKey = $request->query('targetingKey');
+
+ return $this->html($this->render('flags', [
+ 'overview' => $overview,
+ 'flags' => [],
+ 'writable' => $writable,
+ 'detail' => $detail,
+ 'definitionJson' => (string) json_encode($definition, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION),
+ 'variants' => $variants,
+ 'layerRows' => $layers,
+ 'context' => is_string($context) ? $context : '',
+ 'targetingKey' => is_string($targetingKey) ? $targetingKey : '',
+ 'evaluation' => $request->query('evaluate') !== null
+ ? $this->flagsEvaluation($key, is_string($context) ? $context : '', is_string($targetingKey) ? $targetingKey : '')
+ : null,
+ ], $current), 200);
+ }
+
+ private function flagsWrite(Request $request): RedirectResponse
+ {
+ $list = $this->settings->url('flags');
+ $key = $request->input('flag');
+ $op = $request->input('op');
+
+ if (! is_string($key) || $key === '' || ! in_array($op, ['enable', 'disable', 'default-variant', 'put', 'delete'], true)) {
+ return $this->redirect($list, 'Refused: no flag or no operation was named.');
+ }
+
+ $back = $request->input('back') === 'list' ? $list : $list.'?flag='.rawurlencode($key);
+ $body = ['action' => $op];
+ $expected = $request->input('expectedVersion');
+ if ($request->exists('expectedVersion')) {
+ $digits = is_int($expected) && $expected >= 0 ? (string) $expected : $expected;
+ if (! is_string($digits) || preg_match('/^[0-9]+$/D', $digits) !== 1) {
+ return $this->redirect($back, 'Refused: expectedVersion must be a nonnegative integer within the supported range.');
+ }
+ $digits = ltrim($digits, '0');
+ $limit = (string) PHP_INT_MAX;
+ if (strlen($digits) > strlen($limit) || (strlen($digits) === strlen($limit) && strcmp($digits, $limit) > 0)) {
+ return $this->redirect($back, 'Refused: expectedVersion must be a nonnegative integer within the supported range.');
+ }
+ $body['expectedVersion'] = (int) $digits;
+ }
+
+ $raw = null;
+ if ($op === 'default-variant') {
+ $variant = $request->input('variant');
+ $body['variant'] = is_string($variant) ? $variant : '';
+ }
+ if ($op === 'put') {
+ $definition = $request->input('definition');
+ if (! is_string($definition) || ! str_starts_with(ltrim($definition), '{') || ! json_validate($definition)) {
+ return $this->redirect($back, 'Refused: the definition is not a JSON object.');
+ }
+ $raw = '{"action":"put","definition":'.trim($definition)
+ .(isset($body['expectedVersion']) ? ',"expectedVersion":'.$body['expectedVersion'] : '').'}';
+ }
+
+ $response = $this->reader->call('POST', 'flags', [$key], [], $body, $raw);
+ if ($response === null) {
+ return $this->redirect($back, 'Refused: the flags endpoint is unavailable.');
+ }
+
+ $answer = is_string($response->body) ? json_decode($response->body, true) : null;
+ if ($response->status >= 300) {
+ return $this->redirect($back, 'Refused: '.(is_array($answer) && is_string($answer['message'] ?? null) ? $answer['message'] : 'the write was not accepted.'));
+ }
+
+ if (is_array($answer) && ($answer['deleted'] ?? false) === true) {
+ return $this->redirect($list, "Deleted [{$key}].");
+ }
+
+ if (is_array($answer) && ($answer['refreshPending'] ?? false) === true) {
+ return $this->redirect($back, "Write accepted for [{$key}]. Refresh pending; check this page again for visibility.");
+ }
+
+ return $this->redirect($back, match ($op) {
+ 'enable' => "Enabled [{$key}].",
+ 'disable' => "Disabled [{$key}].",
+ 'default-variant' => "Set the default variant of [{$key}].",
+ 'put' => "Saved [{$key}].",
+ default => "Deleted the stored override of [{$key}].",
+ });
+ }
+
+ private function flagsUnavailable(AdminPage $current): SymfonyResponse
+ {
+ return $this->html($this->render('flags', [
+ 'overview' => [], 'flags' => [], 'detail' => null, 'writable' => false, 'unavailable' => true,
+ ], $current), 503);
+ }
+
+ /** @return array|null */
+ private function flagsOverview(): ?array
+ {
+ $response = $this->reader->call('GET', 'flags');
+ $body = $response !== null && $response->status === 200 && is_string($response->body) ? json_decode($response->body, true) : null;
+
+ return is_array($body) && is_array($body['flags'] ?? null) && array_is_list($body['flags'])
+ && is_array($body['sources'] ?? null) && is_array($body['provider'] ?? null)
+ && is_bool($body['writable'] ?? null) && is_bool($body['writesEnabled'] ?? null) ? $body : null;
+ }
+
+ /** @return array{ok: bool, body: array|null, error: string|null, valueJson?: string, valueLabel?: string} */
+ private function flagsEvaluation(string $key, string $context, string $targetingKey): array
+ {
+ $context = trim($context) === '' ? '{}' : trim($context);
+ if (! str_starts_with($context, '{') || ! json_validate($context)) {
+ return ['ok' => false, 'body' => null, 'error' => 'The context is not a JSON object.'];
+ }
+
+ $raw = '{"action":"evaluate","context":'.$context.',"targetingKey":'.($targetingKey === '' ? 'null' : (string) json_encode($targetingKey)).'}';
+ $response = $this->reader->call('POST', 'flags', [$key], [], [], $raw);
+ $body = $response !== null && is_string($response->body) ? json_decode($response->body, true) : null;
+
+ if ($response === null || $response->status !== 200 || ! is_array($body)) {
+ return ['ok' => false, 'body' => null, 'error' => is_array($body) && is_string($body['message'] ?? null) ? $body['message'] : 'The evaluation failed.'];
+ }
+
+ $objects = json_decode($response->body);
+
+ return ['ok' => true, 'body' => $body, 'error' => null,
+ 'valueJson' => $objects instanceof stdClass
+ ? (string) json_encode($objects->value ?? null, JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION)
+ : $response->body,
+ 'valueLabel' => $objects instanceof stdClass ? 'Value' : 'Complete evaluation JSON'];
+ }
+
/**
* The data-layer page.
*
diff --git a/packages/admin/src/Web/AdminPage.php b/packages/admin/src/Web/AdminPage.php
index 7aa2f4cb..410ed80d 100644
--- a/packages/admin/src/Web/AdminPage.php
+++ b/packages/admin/src/Web/AdminPage.php
@@ -70,6 +70,8 @@ public static function all(): array
'The cache stores this application has configured.'),
new self('loggers', 'Loggers', 'loggers', self::GROUP_CONFIG,
'Log channels and their levels.'),
+ new self('flags', 'Feature flags', 'flags', self::GROUP_CONFIG,
+ 'Every feature flag, its source and history, with controls when writes are enabled.'),
// `requires` is null and its own settings decide whether it appears — see AdminAction::nav().
// It is the only page that CHANGES the application rather than describing it, which is why it is
// off by default and refused outright in production.
diff --git a/packages/admin/tests/AdminEndpointReaderCallTest.php b/packages/admin/tests/AdminEndpointReaderCallTest.php
new file mode 100644
index 00000000..bac51bd8
--- /dev/null
+++ b/packages/admin/tests/AdminEndpointReaderCallTest.php
@@ -0,0 +1,55 @@
+broken) {
+ throw new RuntimeException('down');
+ }
+
+ $this->seen = $request;
+
+ return EndpointResponse::text('{"error":"conflict","message":"m"}', 409, 'application/json');
+ }
+ };
+ $registry = new ActuatorRegistry;
+ $registry->register($endpoint);
+ $reader = new AdminEndpointReader($registry, new Config(new Repository([])));
+
+ $response = $reader->call('POST', 'flags', ['k'], [], ['action' => 'put'], '{"action":"put"}');
+
+ expect([$response?->status, $response?->body])->toBe([409, '{"error":"conflict","message":"m"}'])
+ ->and([$endpoint->seen?->method, $endpoint->seen?->subPath, $endpoint->seen?->query, $endpoint->seen?->rawBody, $endpoint->seen?->origin])
+ ->toBe(['POST', ['k'], [], '{"action":"put"}', 'admin'])
+ ->and($reader->call('GET', 'nothing'))->toBeNull();
+
+ $endpoint->broken = true;
+
+ expect($reader->call('GET', 'flags'))->toBeNull();
+});
diff --git a/packages/admin/tests/AdminFlagsPageTest.php b/packages/admin/tests/AdminFlagsPageTest.php
new file mode 100644
index 00000000..f55a76ae
--- /dev/null
+++ b/packages/admin/tests/AdminFlagsPageTest.php
@@ -0,0 +1,286 @@
+ */
+ public array $requests = [];
+
+ public int $writeStatus = 200;
+
+ public bool $pending = false;
+
+ public bool $writable = true;
+
+ public bool $writesEnabled = true;
+
+ public bool $available = true;
+
+ public bool $external = false;
+
+ public ?EndpointResponse $overviewResponse = null;
+
+ public ?EndpointResponse $detailResponse = null;
+
+ public bool $throwOnRead = false;
+
+ public string $previewValue = '{"t":"x","w":1.0}';
+
+ public function endpointId(): string
+ {
+ return 'flags';
+ }
+
+ public function enabled(): bool
+ {
+ return $this->available;
+ }
+
+ public function handle(EndpointRequest $request): EndpointResponse
+ {
+ $this->requests[] = $request;
+ $key = $request->subPath[0] ?? null;
+ if ($request->method === 'GET' && $this->throwOnRead) {
+ throw new RuntimeException('Read unavailable');
+ }
+ if ($request->method === 'GET' && $key === null && $this->overviewResponse !== null) {
+ return $this->overviewResponse;
+ }
+ if ($request->method === 'GET' && $key !== null && $this->detailResponse !== null) {
+ return $this->detailResponse;
+ }
+
+ if ($request->method === 'GET' && $key === null) {
+ return EndpointResponse::text('{"provider":{"name":"firefly","status":"READY"},"writable":'.($this->writable ? 'true' : 'false').',"writesEnabled":'.($this->writesEnabled ? 'true' : 'false').','
+ .'"sources":[{"name":"'.($this->external ? 'http' : 'config').'","enabled":true,"status":"UP","flags":2,"lastRefresh":"2026-10-01T09:30:00Z","error":null,"revision":null}],'
+ .'"flags":[{"key":"banner","state":"ENABLED","type":"object","variants":["none","promo"],"defaultVariant":"none","targeting":false,"origin":"store","overrides":["config"],"metadata":{},"expired":false,"version":3},'
+ .'{"key":"legacy-export","state":"ENABLED","type":"boolean","variants":["on","off"],"defaultVariant":"on","targeting":false,"origin":"config","overrides":[],"metadata":{"expires":"2025-01-01"},"expired":true,"version":null}]}', 200, 'application/json');
+ }
+
+ if ($request->method === 'GET' && $key === 'banner') {
+ return EndpointResponse::text('{"key":"banner","definition":{"state":"ENABLED","variants":{"none":{},"promo":{"t":"x","w":1.0}},"defaultVariant":"none"},"origin":"store",'
+ .'"layers":[{"source":"store","definition":{"state":"ENABLED","variants":{"none":{},"promo":{"t":"x","w":1.0}},"defaultVariant":"none"}}],'
+ .'"version":3,"expired":false,"history":[{"id":9,"action":"put","actor":"ada","changedAt":"2026-10-01T09:30:00Z"}]}', 200, 'application/json');
+ }
+
+ if ($request->method === 'POST' && $request->rawBody !== null && str_contains($request->rawBody, '"evaluate"')) {
+ return EndpointResponse::text('{"key":"banner","value":'.$this->previewValue.',"variant":"promo","reason":"TARGETING_MATCH","errorCode":null,"metadata":{}}', 200, 'application/json');
+ }
+
+ if ($request->method === 'POST') {
+ if ($this->writeStatus !== 200) {
+ return EndpointResponse::text('{"error":"conflict","message":"Flag [banner] is at version 4, the write expected 3."}', $this->writeStatus, 'application/json');
+ }
+
+ return EndpointResponse::text($this->pending ? '{"key":"banner","refreshPending":true}' : '{"key":"banner"}', 200, 'application/json');
+ }
+
+ return EndpointResponse::text('{"error":"unknown-flag","message":"No layer defines flag ['.$key.']."}', 404, 'application/json');
+ }
+}
+
+function featureFlagsAdminScripted(): AdminFlagsScriptedEndpoint
+{
+ return AdminFlagsScriptedEndpoint::$current ?? throw new LogicException('beforeEach registers the scripted endpoint.');
+}
+
+function featureFlagsAdminLastRequest(): EndpointRequest
+{
+ $last = end(featureFlagsAdminScripted()->requests);
+
+ return $last instanceof EndpointRequest ? $last : throw new LogicException('No endpoint request was recorded.');
+}
+
+beforeEach(function (): void {
+ /** @var AdminCapstoneTestCase $this */
+ AdminFlagsScriptedEndpoint::$current = new AdminFlagsScriptedEndpoint;
+ /** @var ActuatorRegistry $registry */
+ $registry = $this->app()->make(ActuatorRegistry::class);
+ $registry->register(AdminFlagsScriptedEndpoint::$current);
+});
+
+it('lists flags with state, origin, expiry and a toggle', function (): void {
+ /** @var AdminCapstoneTestCase $this */
+ $this->get('/firefly/flags')->assertOk()->assertSee('Feature flags')->assertSee('banner')
+ ->assertSee('legacy-export')->assertSee('expired')->assertSee('Disable')->assertSee('2026-10-01T09:30:00Z')
+ ->assertSee('name="expectedVersion" value="3"', false);
+});
+
+it('shows exact definition shapes, layers and history', function (): void {
+ /** @var AdminCapstoneTestCase $this */
+ $this->get('/firefly/flags?flag=banner')->assertOk()->assertSee('"none": {}')->assertSee('"w": 1.0')
+ ->assertSee('Save definition')->assertSee('Delete stored override')->assertSee('ada ', false)
+ ->assertSee('name="expectedVersion" value="3"', false);
+});
+
+it('answers 404 for an unknown key', function (): void {
+ /** @var AdminCapstoneTestCase $this */
+ $this->get('/firefly/flags?flag=nope')->assertNotFound()->assertSee('No such flag');
+});
+
+it('previews an evaluation with typed context and trusted admin origin', function (): void {
+ /** @var AdminCapstoneTestCase $this */
+ $this->get('/firefly/flags?flag=banner&evaluate=1&context='.rawurlencode('{"plan":"pro"}').'&targetingKey=u-1')
+ ->assertOk()->assertSee('TARGETING_MATCH');
+
+ $evaluate = featureFlagsAdminLastRequest();
+ expect($evaluate->rawBody)->toBe('{"action":"evaluate","context":{"plan":"pro"},"targetingKey":"u-1"}')
+ ->and($evaluate->query)->toBe([])
+ ->and($evaluate->origin)->toBe('admin');
+});
+
+it('posts a toggle in-process and reports its result', function (): void {
+ /** @var AdminCapstoneTestCase $this */
+ $this->post('/firefly/flags', ['flag' => 'banner', 'op' => 'disable', 'back' => 'list'])
+ ->assertRedirect('/firefly/flags')->assertSessionHas('data-message', 'Disabled [banner].');
+
+ $write = featureFlagsAdminLastRequest();
+ expect([$write->method, $write->subPath, $write->query, $write->body, $write->origin])
+ ->toBe(['POST', ['banner'], [], ['action' => 'disable'], 'admin']);
+});
+
+it('sends the editor JSON untouched as the raw body', function (): void {
+ /** @var AdminCapstoneTestCase $this */
+ $this->post('/firefly/flags', ['flag' => 'banner', 'op' => 'put', 'expectedVersion' => '3',
+ 'definition' => '{"state":"ENABLED","variants":{"none":{}},"defaultVariant":"none"}'])
+ ->assertRedirect('/firefly/flags?flag=banner')->assertSessionHas('data-message', 'Saved [banner].');
+
+ expect(featureFlagsAdminLastRequest()->rawBody)
+ ->toBe('{"action":"put","definition":{"state":"ENABLED","variants":{"none":{}},"defaultVariant":"none"},"expectedVersion":3}');
+});
+
+it('reports a pending refresh as a successful write with polling guidance', function (): void {
+ /** @var AdminCapstoneTestCase $this */
+ featureFlagsAdminScripted()->pending = true;
+ $this->post('/firefly/flags', ['flag' => 'banner', 'op' => 'put', 'definition' => '{"state":"ENABLED","variants":{"none":{}},"defaultVariant":"none"}'])
+ ->assertSessionHas('data-message', 'Write accepted for [banner]. Refresh pending; check this page again for visibility.');
+});
+
+it('reports endpoint refusals and invalid definitions', function (): void {
+ /** @var AdminCapstoneTestCase $this */
+ featureFlagsAdminScripted()->writeStatus = 409;
+ $this->post('/firefly/flags', ['flag' => 'banner', 'op' => 'enable'])
+ ->assertSessionHas('data-message', 'Refused: Flag [banner] is at version 4, the write expected 3.');
+ $this->post('/firefly/flags', ['flag' => 'banner', 'op' => 'put', 'definition' => '[1, 2]'])
+ ->assertSessionHas('data-message', 'Refused: the definition is not a JSON object.');
+ $this->post('/firefly/flags', ['op' => 'enable'])
+ ->assertSessionHas('data-message', 'Refused: no flag or no operation was named.');
+});
+
+it('shows an external source without offering controls when there is no store', function (): void {
+ /** @var AdminCapstoneTestCase $this */
+ featureFlagsAdminScripted()->writable = false;
+ featureFlagsAdminScripted()->external = true;
+
+ $this->get('/firefly/flags')->assertOk()->assertSee('Read-only: no flag store is configured.')
+ ->assertSee('http')->assertDontSee('Disable');
+ $this->get('/firefly/flags?flag=banner')->assertOk()->assertDontSee('Save definition')
+ ->assertDontSee('Delete stored override')->assertSee('disabled', false);
+});
+
+it('hides controls when writes are disabled and hides the page when the endpoint is disabled', function (): void {
+ /** @var AdminCapstoneTestCase $this */
+ featureFlagsAdminScripted()->writesEnabled = false;
+ $this->get('/firefly/flags')->assertOk()->assertSee('Read-only: enable')
+ ->assertDontSee('Disable');
+
+ featureFlagsAdminScripted()->available = false;
+ $this->get('/firefly/flags')->assertNotFound()->assertSee('This page has no endpoint to read');
+});
+
+it('escapes an unknown key before placing it in the page', function (): void {
+ /** @var AdminCapstoneTestCase $this */
+ $this->get('/firefly/flags?flag='.rawurlencode(''))
+ ->assertNotFound()->assertSee('<script>alert(1)</script>', false)
+ ->assertDontSee('', false);
+});
+
+it('rejects a tokenless POST through the real CSRF middleware', function (): void {
+ /** @var AdminCapstoneTestCase $this */
+ // Laravel bypasses CSRF in the testing environment. Switch only this application's environment so
+ // this request takes the same middleware branch as deployed HTTP traffic.
+ $this->app()->instance('env', 'local');
+ $this->post('/firefly/flags', ['flag' => 'banner', 'op' => 'disable'])->assertStatus(419);
+
+ expect(featureFlagsAdminScripted()->requests)->toBe([]);
+});
+
+it('refuses a malformed submitted version before endpoint dispatch', function (mixed $version): void {
+ /** @var AdminCapstoneTestCase $this */
+ $this->post('/firefly/flags', ['flag' => 'banner', 'op' => 'disable', 'expectedVersion' => $version])
+ ->assertSessionHas('data-message', 'Refused: expectedVersion must be a nonnegative integer within the supported range.');
+ expect(featureFlagsAdminScripted()->requests)->toBe([]);
+})->with(['text' => ['oops'], 'negative' => ['-1'], 'array' => [['3']], 'overflow' => [(string) PHP_INT_MAX.'0'], 'empty' => [''], 'null' => [null]]);
+
+it('preserves an explicit zero optimistic version', function (): void {
+ /** @var AdminCapstoneTestCase $this */
+ $this->post('/firefly/flags', ['flag' => 'banner', 'op' => 'enable', 'expectedVersion' => '0'])->assertRedirect();
+ expect(featureFlagsAdminLastRequest()->body['expectedVersion'] ?? null)->toBe(0);
+});
+
+it('reports unavailable reads without inventing empty or missing flags', function (bool $detail, string $failure): void {
+ /** @var AdminCapstoneTestCase $this */
+ $endpoint = featureFlagsAdminScripted();
+ if ($failure === 'throw') {
+ $endpoint->throwOnRead = true;
+ } else {
+ $response = EndpointResponse::text($failure === 'malformed' ? '{broken' : ($failure === 'shape' ? '{}' : 'unavailable'), $failure === 'failed' ? 503 : 200);
+ if ($detail) {
+ $endpoint->detailResponse = $response;
+ } else {
+ $endpoint->overviewResponse = $response;
+ }
+ }
+ $this->get('/firefly/flags'.($detail ? '?flag=banner' : ''))->assertStatus(503)
+ ->assertSee('Feature flag data is unavailable')->assertDontSee('No flags defined')->assertDontSee('No such flag')
+ ->assertDontSee('no flag store is configured')->assertDontSee('Save definition')->assertDontSee('Disable');
+})->with([false, true])->with(['throw', 'failed', 'malformed', 'shape']);
+
+it('renders a genuinely empty overview as empty', function (): void {
+ /** @var AdminCapstoneTestCase $this */
+ featureFlagsAdminScripted()->overviewResponse = EndpointResponse::text('{"flags":[],"sources":[],"provider":{},"writable":false,"writesEnabled":false}', 200);
+ $this->get('/firefly/flags')->assertOk()->assertSee('No flags defined')->assertDontSee('Feature flag data is unavailable');
+});
+
+it('renders the exact JSON shape of preview values', function (string $value): void {
+ /** @var AdminCapstoneTestCase $this */
+ featureFlagsAdminScripted()->previewValue = $value;
+ $this->get('/firefly/flags?flag=banner&evaluate=1')->assertOk()
+ ->assertSee('data-evaluation="value">'.e($value).'', false);
+})->with(['{}', '[]', '{"0":{},"1":[{},[],1.0]}', '1.0']);
+
+it('shows a complete faithful preview when PHP cannot represent a member as an object property', function (): void {
+ /** @var AdminCapstoneTestCase $this */
+ featureFlagsAdminScripted()->previewValue = '{"\\u0000nested":{"0":{},"list":[],"float":1.0}}';
+ $this->get('/firefly/flags?flag=banner&evaluate=1')->assertOk()->assertSee('Complete evaluation JSON')
+ ->assertSee(featureFlagsAdminScripted()->previewValue)->assertDontSee('data-evaluation="value">null', false);
+});
+
+it('shows faithful complete details without offering a lossy definition editor', function (): void {
+ /** @var AdminCapstoneTestCase $this */
+ $definition = '{"state":"ENABLED","variants":{"v":{"\\u0000nested":{"0":{},"list":[],"float":1.0}}},"defaultVariant":"v"}';
+ featureFlagsAdminScripted()->detailResponse = EndpointResponse::text('{"key":"banner","definition":'.$definition.',"layers":[],"history":[],"origin":"store","version":3}', 200);
+ $this->get('/firefly/flags?flag=banner')->assertOk()->assertSee('Complete flag detail JSON')->assertSee($definition)
+ ->assertDontSee('Save definition')->assertDontSee('name="definition"', false)->assertDontSee('Feature flag data is unavailable');
+});
+
+it('forwards valid NUL members in raw definition and preview context without object decoding', function (): void {
+ /** @var AdminCapstoneTestCase $this */
+ $definition = '{"state":"ENABLED","variants":{"v":{"\\u0000name":{}}},"defaultVariant":"v"}';
+ $this->post('/firefly/flags', ['flag' => 'banner', 'op' => 'put', 'definition' => $definition])
+ ->assertSessionHas('data-message', 'Saved [banner].');
+ expect(featureFlagsAdminLastRequest()->rawBody)->toBe('{"action":"put","definition":'.$definition.'}');
+ $this->get('/firefly/flags?flag=banner&evaluate=1&context='.rawurlencode('{"\\u0000name":1.0}'))->assertOk()->assertSee('TARGETING_MATCH');
+ expect(featureFlagsAdminLastRequest()->rawBody)->toContain('"context":{"\\u0000name":1.0}');
+});
diff --git a/packages/cli/composer.json b/packages/cli/composer.json
index 6694ebd4..82f1e05b 100644
--- a/packages/cli/composer.json
+++ b/packages/cli/composer.json
@@ -29,6 +29,7 @@
"firefly/cqrs": "*@dev",
"firefly/data": "*@dev",
"firefly/eda": "*@dev",
+ "firefly/feature-flags": "*@dev",
"firefly/kernel": "*@dev",
"firefly/messaging": "*@dev",
"firefly/observability": "*@dev",
diff --git a/packages/cli/src/Cache/ManifestCacheWriter.php b/packages/cli/src/Cache/ManifestCacheWriter.php
index 6a1e718a..99d5c6fd 100644
--- a/packages/cli/src/Cache/ManifestCacheWriter.php
+++ b/packages/cli/src/Cache/ManifestCacheWriter.php
@@ -17,6 +17,7 @@
use Firefly\Data\Transaction\TransactionalManifestCompiler;
use Firefly\Eda\Listener\EventListenerManifestCompiler;
use Firefly\Eda\Scanner\EventListenerScanner;
+use Firefly\FeatureFlags\Gating\FeatureFlagAdviceSource;
use Firefly\Messaging\Listener\MessageListenerManifestCompiler;
use Firefly\Messaging\Scanner\MessageListenerScanner;
use Firefly\Observability\Method\ObservabilityAdviceSource;
@@ -97,15 +98,16 @@ public function writeProxies(array $psr4, string $dir): int
}
/**
- * The advice every proxy runs, in the order the plan will chain it: metrics (50) outside security (100)
- * outside resilience (200) outside transactions (1000). D4 direct dependency like every other scanner
- * here — the uncached boot collects the same four sources as #[Component]s through Container::getAll(),
+ * The advice every proxy runs, in the order the plan will chain it: metrics (50) outside feature flags (80)
+ * outside security (100) outside resilience (200) outside transactions (1000). D4 direct dependency like
+ * every other scanner here — the uncached boot collects the same five sources as #[Component]s through Container::getAll(),
* so a cached app and a dev app enforce an identical plan.
*/
private function planner(): ProxyPlanner
{
return new ProxyPlanner([
new ObservabilityAdviceSource,
+ new FeatureFlagAdviceSource,
new MethodSecurityAdviceSource,
new ResilienceAdviceSource,
new TransactionalAdviceSource,
diff --git a/packages/cli/tests/Cache/CachedBootTest.php b/packages/cli/tests/Cache/CachedBootTest.php
index 313881b4..c12fc00e 100644
--- a/packages/cli/tests/Cache/CachedBootTest.php
+++ b/packages/cli/tests/Cache/CachedBootTest.php
@@ -15,6 +15,9 @@
use Firefly\Data\Proxy\Advice;
use Firefly\Data\Proxy\ProxyPlan;
use Firefly\Data\Transaction\TransactionalManifest;
+use Firefly\FeatureFlags\FeatureFlagsServiceProvider;
+use Firefly\FeatureFlags\Gating\FeatureFlagAdviceSource;
+use Firefly\FeatureFlags\Gating\FeatureFlagDisabledException;
use Firefly\Observability\Method\ObservabilityAdviceSource;
use Firefly\Resilience\Method\ResilienceAdviceSource;
use Firefly\Security\Access\Method\MethodSecurityAdviceSource;
@@ -73,7 +76,7 @@ function cachedBootPsr4(): array
->and($plan->hasProxyFor(DemoSecuredService::class))->toBeTrue()
->and(array_keys($plan->adviceFor(DemoSecuredService::class)))->toBe([MethodSecurityAdviceSource::ID])
->and($plan->methodsFor(DemoSecuredService::class)['secret'][0]['row']['expression'])->toBe("hasRole('ADMIN')")
- // …and the same for the metric-only #[Service]: the third source in planner(), which nothing else
+ // …and the same for the metric-only #[Service]: its source in planner(), which nothing else
// in this package's suite exercises. Deleting `new ObservabilityAdviceSource` from planner() used to
// leave the whole suite green while a cached app recorded no method meter at all.
->and($plan->hasProxyFor(DemoTimedService::class))->toBeTrue()
@@ -103,7 +106,7 @@ function cachedBootPsr4(): array
| The chain, on the compiled artifact. ObservabilityAdviceSource's order 50 is the constant every claim about
| #[Timed] rests on — "a refusal is still counted", "the commit is inside the timer" — and a class carrying
| ONE advice can never contradict it, which is what every other fixture here carries. DemoLayeredService
- | carries all four on one method, so ProxyPlanner's sort and ProxyPlan::adviceFor()'s sort both have to
+ | carries all five on one method, so ProxyPlanner's sort and ProxyPlan::adviceFor()'s sort both have to
| agree on the whole sequence before this passes. Changing 50 to 150 leaves every other test in the
| monorepo green and turns method metrics into a meter that goes quiet exactly when an operator needs it.
|
@@ -114,7 +117,7 @@ function cachedBootPsr4(): array
| application lost all six resilience attributes — the proxies still generated, just with no resilience
| advice in them at all.
*/
-it('chains metrics OUTSIDE security, security outside resilience and resilience outside the transaction', function () {
+it('chains metrics outside feature flags, feature flags outside security, security outside resilience and resilience outside the transaction', function () {
$dir = sys_get_temp_dir().'/firefly-cache-'.bin2hex(random_bytes(6));
(new ManifestCacheWriter)->write(cachedBootPsr4(), $dir);
@@ -123,17 +126,19 @@ function cachedBootPsr4(): array
$advice = $plan->adviceFor(DemoLayeredService::class);
// The advice set on the class, outermost first, and the orders that put it in that sequence.
- expect(array_keys($advice))->toBe([ObservabilityAdviceSource::ID, MethodSecurityAdviceSource::ID, ResilienceAdviceSource::ID, Advice::TRANSACTIONAL])
+ expect(array_keys($advice))->toBe([ObservabilityAdviceSource::ID, FeatureFlagAdviceSource::ID, MethodSecurityAdviceSource::ID, ResilienceAdviceSource::ID, Advice::TRANSACTIONAL])
->and($advice[ObservabilityAdviceSource::ID]->order)->toBe(50)
+ ->and($advice[FeatureFlagAdviceSource::ID]->order)->toBe(80)
->and($advice[ResilienceAdviceSource::ID]->order)->toBe(200)
- ->and($advice[ObservabilityAdviceSource::ID]->order)->toBeLessThan($advice[MethodSecurityAdviceSource::ID]->order)
+ ->and($advice[ObservabilityAdviceSource::ID]->order)->toBeLessThan($advice[FeatureFlagAdviceSource::ID]->order)
+ ->and($advice[FeatureFlagAdviceSource::ID]->order)->toBeLessThan($advice[MethodSecurityAdviceSource::ID]->order)
->and($advice[MethodSecurityAdviceSource::ID]->order)->toBeLessThan($advice[ResilienceAdviceSource::ID]->order)
->and($advice[ResilienceAdviceSource::ID]->order)->toBeLessThan($advice[Advice::TRANSACTIONAL]->order);
// …and the per-method rows, which are what ProxyPlanner::proxyMethods() turns into the generated chain:
// the same sequence, so the emitted proceed() really does reach the metric link first.
expect(array_column($plan->methodsFor(DemoLayeredService::class)['all'], 'advice'))
- ->toBe([ObservabilityAdviceSource::ID, MethodSecurityAdviceSource::ID, ResilienceAdviceSource::ID, Advice::TRANSACTIONAL]);
+ ->toBe([ObservabilityAdviceSource::ID, FeatureFlagAdviceSource::ID, MethodSecurityAdviceSource::ID, ResilienceAdviceSource::ID, Advice::TRANSACTIONAL]);
// ResilienceAdviceSource::render() runs on the CACHED path alone — the resilience capstones take the
// scan branch by design — so this is the only place the emitted descriptor literal is proved to parse
@@ -144,7 +149,8 @@ function cachedBootPsr4(): array
expect($layeredProxy)->toContain('__fireflyResilienceInterceptor')
->and($layeredProxy)->toContain('\\Firefly\\Resilience\\Method\\ResilienceMethodDescriptor::fromArray(')
- ->and($plan->methodsFor(DemoLayeredService::class)['all'][2]['row']['retry'])->toBe('demo');
+ ->and($layeredProxy)->toContain('\\Firefly\\FeatureFlags\\Gating\\FeatureFlagMethodDescriptor::fromArray(')
+ ->and($plan->methodsFor(DemoLayeredService::class)['all'][3]['row']['retry'])->toBe('demo');
});
it('boots the fixture app on the CACHED zero-reflection path with a working #[Transactional] proxy', function () {
@@ -169,6 +175,7 @@ function cachedBootPsr4(): array
'component_manifest' => $dir.'/'.FireflyCachePaths::COMPONENT,
'context_manifest' => $dir.'/'.FireflyCachePaths::CONTEXT,
],
+ 'feature-flags' => ['enabled' => true, 'flags' => ['demo-layered' => false]],
],
// Seeds a value that DIFFERS from DemoConfigProperties's constructor default ('hello'), so
// assertion (d) below can distinguish "the DTO was POPULATED FROM CONFIG" from "the DTO merely
@@ -180,6 +187,7 @@ function cachedBootPsr4(): array
ValidationServiceProvider::class,
WebServiceProvider::class,
DataServiceProvider::class,
+ FeatureFlagsServiceProvider::class,
FireflyCacheServiceProvider::class,
],
// ValidationAutoConfiguration's validator() bean is an eager singleton needing the Illuminate validation
@@ -247,6 +255,10 @@ function cachedBootAssertions(Application $app): void
expect($timed::class)->toBe(DemoTimedService::class.ProxyPlan::PROXY_SUFFIX)
->and($timed->measured())->toBe('measured');
+ /** @var DemoLayeredService $layered */
+ $layered = $context->get(DemoLayeredService::class);
+ expect(fn () => $layered->all('dark'))->toThrow(FeatureFlagDisabledException::class);
+
// Category C: the compiled TransactionalManifest is populated (the #[Configuration] #[Bean] loaded it).
/** @var TransactionalManifest $manifest */
$manifest = $context->get(TransactionalManifest::class);
diff --git a/packages/cli/tests/Fixtures/App/DemoLayeredService.php b/packages/cli/tests/Fixtures/App/DemoLayeredService.php
index 57b04e19..ab59a2c4 100644
--- a/packages/cli/tests/Fixtures/App/DemoLayeredService.php
+++ b/packages/cli/tests/Fixtures/App/DemoLayeredService.php
@@ -6,21 +6,22 @@
use Firefly\Container\Attributes\Service;
use Firefly\Data\Transaction\Attributes\Transactional;
+use Firefly\FeatureFlags\Gating\FeatureFlag;
use Firefly\Observability\Method\Timed;
use Firefly\Resilience\Method\Retry;
use Firefly\Security\Access\Attributes\PreAuthorize;
/**
- * The one fixture that carries ALL FOUR advice kinds on a single method, so the compiled plan has something
+ * The one fixture that carries ALL FIVE advice kinds on a single method, so the compiled plan has something
* to say about their relative order. DemoTimedService, DemoSecuredService and DemoTransactionalService each
* prove that their own AdviceSource reaches planner(); none of them can prove that metrics (50) is chained
- * OUTSIDE security (100), which is outside resilience (200), which is outside the transaction (1000),
+ * OUTSIDE feature flags (80), which is outside security (100), which is outside resilience (200), which is outside the transaction (1000),
* because a class with one advice has no chain.
*
* The number 50 is the single most load-bearing constant in firefly/observability's method metrics — the
* whole argument for #[Timed] being worth anything under a permissions misconfiguration rests on the metric
* link running before MethodSecurityInterceptor throws — and it was, until this fixture, justified only in a
- * docblock. #[Retry] is here for the same reason and with the same history: planner() names four
+ * docblock. #[Retry] is here for the same reason and with the same history: planner() names five
* AdviceSources and this package's suite could not tell whether the fourth was in the list at all, so
* deleting `new ResilienceAdviceSource` left every test green while every cached application silently lost
* all six resilience attributes. NOT final: the proxy extends it.
@@ -29,6 +30,7 @@
class DemoLayeredService
{
#[Timed('demo.layered')]
+ #[FeatureFlag('demo-layered', default: true)]
#[PreAuthorize("hasRole('ADMIN')")]
#[Retry('demo')]
#[Transactional]
diff --git a/packages/feature-flags/.gitattributes b/packages/feature-flags/.gitattributes
new file mode 100644
index 00000000..538b69a4
--- /dev/null
+++ b/packages/feature-flags/.gitattributes
@@ -0,0 +1,2 @@
+/tests export-ignore
+/.gitattributes export-ignore
diff --git a/packages/feature-flags/LICENSE b/packages/feature-flags/LICENSE
new file mode 100644
index 00000000..2240005d
--- /dev/null
+++ b/packages/feature-flags/LICENSE
@@ -0,0 +1,204 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+Copyright 2026 Firefly Software Solutions Inc.
diff --git a/packages/feature-flags/README.md b/packages/feature-flags/README.md
new file mode 100644
index 00000000..a2dfaed9
--- /dev/null
+++ b/packages/feature-flags/README.md
@@ -0,0 +1,14 @@
+# firefly/feature-flags
+
+Feature flags for LaraFly on OpenFeature. Flags are flagd flag definitions — the format the OpenFeature
+ecosystem shares — evaluated in-process with the same semantics PyFly uses, so a user gets the same
+variant and the same percentage bucket in a PHP service and in a Python one. Definitions come from
+`firefly.feature-flags.flags`, a watched flagd file, another service's sync endpoint and a writable store;
+the highest layer that defines a key wins. Code reads flags through the `FeatureFlags` service or any
+OpenFeature client, gates a bean method or a route with `#[FeatureFlag]`, and a Blade view with
+`@featureflag`. Operators change flags at runtime from the admin dashboard, the `flags` actuator endpoint
+and `php artisan firefly:flags`, with every change recorded.
+
+See [Feature Flags](../../docs/modules/feature-flags.md) for the full guide.
+
+Apache-2.0 © Firefly Software Solutions Inc.
diff --git a/packages/feature-flags/cache/firefly-feature-flags-components.php b/packages/feature-flags/cache/firefly-feature-flags-components.php
new file mode 100644
index 00000000..65755771
--- /dev/null
+++ b/packages/feature-flags/cache/firefly-feature-flags-components.php
@@ -0,0 +1,441 @@
+ [
+ 'class' => 'Firefly\\FeatureFlags\\Boot\\FeatureFlagsLifecycle',
+ 'stereotype' => 'component',
+ 'name' => null,
+ 'scope' => 'Singleton',
+ 'primary' => false,
+ 'order' => 0,
+ 'qualifier' => null,
+ 'interfaces' => [
+ 0 => 'Firefly\\Kernel\\Lifecycle',
+ ],
+ 'beans' => [
+ ],
+ 'lazy' => false,
+ 'dependencies' => [
+ 0 => 'Firefly\\Container\\Container',
+ 1 => 'OpenFeature\\interfaces\\flags\\API',
+ ],
+ ],
+ 1 => [
+ 'class' => 'Firefly\\FeatureFlags\\Context\\ApplicationEvaluationContextContributor',
+ 'stereotype' => 'component',
+ 'name' => null,
+ 'scope' => 'Singleton',
+ 'primary' => false,
+ 'order' => -200,
+ 'qualifier' => null,
+ 'interfaces' => [
+ 0 => 'Firefly\\FeatureFlags\\Context\\EvaluationContextContributor',
+ ],
+ 'beans' => [
+ ],
+ 'lazy' => false,
+ 'dependencies' => [
+ 0 => 'Illuminate\\Contracts\\Config\\Repository',
+ ],
+ ],
+ 2 => [
+ 'class' => 'Firefly\\FeatureFlags\\Evaluation\\FlagdEvaluatorConfiguration',
+ 'stereotype' => 'configuration',
+ 'name' => null,
+ 'scope' => 'Singleton',
+ 'primary' => false,
+ 'order' => 700,
+ 'qualifier' => null,
+ 'interfaces' => [
+ ],
+ 'beans' => [
+ 0 => [
+ 'method' => 'flagdEvaluator',
+ 'returns' => 'Firefly\\FeatureFlags\\Evaluation\\FlagdEvaluator',
+ 'name' => null,
+ 'scope' => 'Singleton',
+ 'primary' => false,
+ 'order' => 0,
+ 'lazy' => false,
+ 'dependencies' => [
+ ],
+ ],
+ ],
+ 'lazy' => false,
+ 'dependencies' => [
+ ],
+ ],
+ 3 => [
+ 'class' => 'Firefly\\FeatureFlags\\FeatureFlagsAutoConfiguration',
+ 'stereotype' => 'configuration',
+ 'name' => null,
+ 'scope' => 'Singleton',
+ 'primary' => false,
+ 'order' => 700,
+ 'qualifier' => null,
+ 'interfaces' => [
+ ],
+ 'beans' => [
+ 0 => [
+ 'method' => 'featureFlagsSettings',
+ 'returns' => 'Firefly\\FeatureFlags\\FeatureFlagsSettings',
+ 'name' => null,
+ 'scope' => 'Singleton',
+ 'primary' => false,
+ 'order' => 0,
+ 'lazy' => false,
+ 'dependencies' => [
+ 0 => 'Firefly\\Config\\Config',
+ ],
+ ],
+ 1 => [
+ 'method' => 'featureFlagMetrics',
+ 'returns' => 'Firefly\\FeatureFlags\\Telemetry\\FeatureFlagMetrics',
+ 'name' => null,
+ 'scope' => 'Singleton',
+ 'primary' => false,
+ 'order' => 0,
+ 'lazy' => false,
+ 'dependencies' => [
+ ],
+ ],
+ 2 => [
+ 'method' => 'flagRegistry',
+ 'returns' => 'Firefly\\FeatureFlags\\Registry\\FlagRegistry',
+ 'name' => null,
+ 'scope' => 'Singleton',
+ 'primary' => false,
+ 'order' => 0,
+ 'lazy' => false,
+ 'dependencies' => [
+ 0 => 'Firefly\\Container\\Container',
+ 1 => 'Illuminate\\Contracts\\Cache\\Repository',
+ 2 => 'Firefly\\Context\\Event\\ApplicationEventPublisher',
+ 3 => 'Psr\\Log\\LoggerInterface',
+ ],
+ ],
+ 3 => [
+ 'method' => 'openFeatureProvider',
+ 'returns' => 'OpenFeature\\interfaces\\provider\\Provider',
+ 'name' => null,
+ 'scope' => 'Singleton',
+ 'primary' => false,
+ 'order' => 0,
+ 'lazy' => false,
+ 'dependencies' => [
+ 0 => 'Firefly\\FeatureFlags\\Registry\\FlagRegistry',
+ 1 => 'Firefly\\FeatureFlags\\Evaluation\\FlagdEvaluator',
+ 2 => 'Psr\\Log\\LoggerInterface',
+ ],
+ ],
+ 4 => [
+ 'method' => 'evaluationContextResolver',
+ 'returns' => 'Firefly\\FeatureFlags\\Context\\EvaluationContextResolver',
+ 'name' => null,
+ 'scope' => 'Singleton',
+ 'primary' => false,
+ 'order' => 0,
+ 'lazy' => false,
+ 'dependencies' => [
+ 0 => 'Firefly\\Container\\Container',
+ 1 => 'Psr\\Log\\LoggerInterface',
+ ],
+ ],
+ 5 => [
+ 'method' => 'featureFlags',
+ 'returns' => 'Firefly\\FeatureFlags\\FeatureFlags',
+ 'name' => null,
+ 'scope' => 'Singleton',
+ 'primary' => false,
+ 'order' => 0,
+ 'lazy' => false,
+ 'dependencies' => [
+ 0 => 'OpenFeature\\interfaces\\provider\\Provider',
+ 1 => 'Firefly\\FeatureFlags\\Context\\EvaluationContextResolver',
+ 2 => 'Firefly\\FeatureFlags\\Telemetry\\FeatureFlagMetrics',
+ 3 => 'Firefly\\Context\\Event\\ApplicationEventPublisher',
+ 4 => 'Firefly\\FeatureFlags\\FeatureFlagsSettings',
+ 5 => 'Firefly\\Container\\Container',
+ 6 => 'Psr\\Log\\LoggerInterface',
+ ],
+ ],
+ 6 => [
+ 'method' => 'featureFlagsClient',
+ 'returns' => 'OpenFeature\\interfaces\\flags\\Client',
+ 'name' => null,
+ 'scope' => 'Singleton',
+ 'primary' => false,
+ 'order' => 0,
+ 'lazy' => false,
+ 'dependencies' => [
+ 0 => 'Firefly\\FeatureFlags\\FeatureFlags',
+ ],
+ ],
+ ],
+ 'lazy' => false,
+ 'dependencies' => [
+ ],
+ ],
+ 4 => [
+ 'class' => 'Firefly\\FeatureFlags\\Gating\\FeatureFlagAdviceSource',
+ 'stereotype' => 'component',
+ 'name' => null,
+ 'scope' => 'Singleton',
+ 'primary' => false,
+ 'order' => 0,
+ 'qualifier' => null,
+ 'interfaces' => [
+ 0 => 'Firefly\\Data\\Proxy\\AdviceSource',
+ ],
+ 'beans' => [
+ ],
+ 'lazy' => false,
+ 'dependencies' => [
+ ],
+ ],
+ 5 => [
+ 'class' => 'Firefly\\FeatureFlags\\Gating\\FeatureFlagsGatingConfiguration',
+ 'stereotype' => 'configuration',
+ 'name' => null,
+ 'scope' => 'Singleton',
+ 'primary' => false,
+ 'order' => 700,
+ 'qualifier' => null,
+ 'interfaces' => [
+ ],
+ 'beans' => [
+ 0 => [
+ 'method' => 'featureFlagGate',
+ 'returns' => 'Firefly\\FeatureFlags\\Gating\\FeatureFlagGate',
+ 'name' => null,
+ 'scope' => 'Singleton',
+ 'primary' => false,
+ 'order' => 0,
+ 'lazy' => false,
+ 'dependencies' => [
+ 0 => 'Firefly\\Container\\Container',
+ 1 => 'Firefly\\Config\\Config',
+ ],
+ ],
+ 1 => [
+ 'method' => 'routeGateDecisions',
+ 'returns' => 'Firefly\\FeatureFlags\\Gating\\RouteGateDecisions',
+ 'name' => null,
+ 'scope' => 'Singleton',
+ 'primary' => false,
+ 'order' => 0,
+ 'lazy' => false,
+ 'dependencies' => [
+ 0 => 'Illuminate\\Contracts\\Container\\Container',
+ ],
+ ],
+ 2 => [
+ 'method' => 'featureFlagMethodInterceptor',
+ 'returns' => 'Firefly\\FeatureFlags\\Gating\\FeatureFlagMethodInterceptor',
+ 'name' => null,
+ 'scope' => 'Singleton',
+ 'primary' => false,
+ 'order' => 0,
+ 'lazy' => false,
+ 'dependencies' => [
+ 0 => 'Firefly\\FeatureFlags\\Gating\\FeatureFlagGate',
+ 1 => 'Firefly\\FeatureFlags\\Gating\\RouteGateDecisions',
+ ],
+ ],
+ ],
+ 'lazy' => false,
+ 'dependencies' => [
+ ],
+ ],
+ 6 => [
+ 'class' => 'Firefly\\FeatureFlags\\Management\\FeatureFlagsHealthIndicator',
+ 'stereotype' => 'component',
+ 'name' => null,
+ 'scope' => 'Singleton',
+ 'primary' => false,
+ 'order' => 0,
+ 'qualifier' => null,
+ 'interfaces' => [
+ 0 => 'Firefly\\Actuator\\Health\\ConditionalHealthIndicator',
+ 1 => 'Firefly\\Actuator\\Health\\HealthIndicator',
+ ],
+ 'beans' => [
+ ],
+ 'lazy' => false,
+ 'dependencies' => [
+ 0 => 'Firefly\\Container\\Container',
+ 1 => 'Closure',
+ ],
+ ],
+ 7 => [
+ 'class' => 'Firefly\\FeatureFlags\\Management\\FeatureFlagsManagementConfiguration',
+ 'stereotype' => 'configuration',
+ 'name' => null,
+ 'scope' => 'Singleton',
+ 'primary' => false,
+ 'order' => 700,
+ 'qualifier' => null,
+ 'interfaces' => [
+ ],
+ 'beans' => [
+ 0 => [
+ 'method' => 'flagManagement',
+ 'returns' => 'Firefly\\FeatureFlags\\Management\\FlagManagement',
+ 'name' => null,
+ 'scope' => 'Singleton',
+ 'primary' => false,
+ 'order' => 0,
+ 'lazy' => false,
+ 'dependencies' => [
+ 0 => 'Firefly\\FeatureFlags\\FeatureFlagsSettings',
+ 1 => 'OpenFeature\\interfaces\\provider\\Provider',
+ 2 => 'Firefly\\Container\\Container',
+ 3 => 'Psr\\Log\\LoggerInterface',
+ ],
+ ],
+ ],
+ 'lazy' => false,
+ 'dependencies' => [
+ ],
+ ],
+ 8 => [
+ 'class' => 'Firefly\\FeatureFlags\\Management\\FlagsEndpoint',
+ 'stereotype' => 'component',
+ 'name' => null,
+ 'scope' => 'Singleton',
+ 'primary' => false,
+ 'order' => 0,
+ 'qualifier' => null,
+ 'interfaces' => [
+ 0 => 'Firefly\\Actuator\\Endpoint\\ActuatorEndpoint',
+ ],
+ 'beans' => [
+ ],
+ 'lazy' => false,
+ 'dependencies' => [
+ 0 => 'Firefly\\Container\\Container',
+ ],
+ ],
+ 9 => [
+ 'class' => 'Firefly\\FeatureFlags\\Source\\ConfigFlagSource',
+ 'stereotype' => 'component',
+ 'name' => null,
+ 'scope' => 'Singleton',
+ 'primary' => false,
+ 'order' => 0,
+ 'qualifier' => null,
+ 'interfaces' => [
+ 0 => 'Firefly\\FeatureFlags\\Source\\FlagSource',
+ ],
+ 'beans' => [
+ ],
+ 'lazy' => false,
+ 'dependencies' => [
+ 0 => 'Firefly\\FeatureFlags\\FeatureFlagsSettings',
+ ],
+ ],
+ 10 => [
+ 'class' => 'Firefly\\FeatureFlags\\Source\\FileFlagSource',
+ 'stereotype' => 'component',
+ 'name' => null,
+ 'scope' => 'Singleton',
+ 'primary' => false,
+ 'order' => 0,
+ 'qualifier' => null,
+ 'interfaces' => [
+ 0 => 'Firefly\\FeatureFlags\\Source\\FlagSource',
+ ],
+ 'beans' => [
+ ],
+ 'lazy' => false,
+ 'dependencies' => [
+ 0 => 'Firefly\\FeatureFlags\\FeatureFlagsSettings',
+ ],
+ ],
+ 11 => [
+ 'class' => 'Firefly\\FeatureFlags\\Source\\HttpFlagSource',
+ 'stereotype' => 'component',
+ 'name' => null,
+ 'scope' => 'Singleton',
+ 'primary' => false,
+ 'order' => 0,
+ 'qualifier' => null,
+ 'interfaces' => [
+ 0 => 'Firefly\\FeatureFlags\\Source\\FlagSource',
+ ],
+ 'beans' => [
+ ],
+ 'lazy' => false,
+ 'dependencies' => [
+ 0 => 'Firefly\\FeatureFlags\\FeatureFlagsSettings',
+ 1 => 'Illuminate\\Http\\Client\\Factory',
+ ],
+ ],
+ 12 => [
+ 'class' => 'Firefly\\FeatureFlags\\Source\\StoreFlagSource',
+ 'stereotype' => 'component',
+ 'name' => null,
+ 'scope' => 'Singleton',
+ 'primary' => false,
+ 'order' => 0,
+ 'qualifier' => null,
+ 'interfaces' => [
+ 0 => 'Firefly\\FeatureFlags\\Source\\FlagSource',
+ ],
+ 'beans' => [
+ ],
+ 'lazy' => false,
+ 'dependencies' => [
+ 0 => 'Firefly\\FeatureFlags\\FeatureFlagsSettings',
+ 1 => 'Firefly\\Container\\Container',
+ ],
+ ],
+ 13 => [
+ 'class' => 'Firefly\\FeatureFlags\\Store\\FeatureFlagStoreConfiguration',
+ 'stereotype' => 'configuration',
+ 'name' => null,
+ 'scope' => 'Singleton',
+ 'primary' => false,
+ 'order' => 700,
+ 'qualifier' => null,
+ 'interfaces' => [
+ ],
+ 'beans' => [
+ 0 => [
+ 'method' => 'flagStore',
+ 'returns' => 'Firefly\\FeatureFlags\\Store\\FlagStore',
+ 'name' => null,
+ 'scope' => 'Singleton',
+ 'primary' => false,
+ 'order' => 0,
+ 'lazy' => false,
+ 'dependencies' => [
+ 0 => 'Firefly\\FeatureFlags\\FeatureFlagsSettings',
+ 1 => 'Illuminate\\Contracts\\Container\\Container',
+ ],
+ ],
+ 1 => [
+ 'method' => 'flagStoreWriter',
+ 'returns' => 'Firefly\\FeatureFlags\\Store\\FlagStoreWriter',
+ 'name' => null,
+ 'scope' => 'Singleton',
+ 'primary' => false,
+ 'order' => 0,
+ 'lazy' => false,
+ 'dependencies' => [
+ 0 => 'Firefly\\FeatureFlags\\Store\\FlagStore',
+ 1 => 'Firefly\\FeatureFlags\\Registry\\FlagRegistry',
+ 2 => 'Firefly\\Context\\Event\\ApplicationEventPublisher',
+ 3 => 'Psr\\Log\\LoggerInterface',
+ ],
+ ],
+ ],
+ 'lazy' => false,
+ 'dependencies' => [
+ ],
+ ],
+];
diff --git a/packages/feature-flags/cache/firefly-feature-flags-context.php b/packages/feature-flags/cache/firefly-feature-flags-context.php
new file mode 100644
index 00000000..9a6d318a
--- /dev/null
+++ b/packages/feature-flags/cache/firefly-feature-flags-context.php
@@ -0,0 +1,523 @@
+ [
+ 'class' => 'Firefly\\FeatureFlags\\Boot\\FeatureFlagsLifecycle',
+ 'postConstruct' => [
+ ],
+ 'preDestroy' => [
+ ],
+ 'listeners' => [
+ ],
+ 'conditions' => [
+ 0 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnProperty',
+ 'args' => [
+ 0 => 'firefly.feature-flags.enabled',
+ 1 => 'true',
+ 2 => false,
+ ],
+ ],
+ ],
+ 'beanConditions' => [
+ ],
+ ],
+ 1 => [
+ 'class' => 'Firefly\\FeatureFlags\\Context\\ApplicationEvaluationContextContributor',
+ 'postConstruct' => [
+ ],
+ 'preDestroy' => [
+ ],
+ 'listeners' => [
+ ],
+ 'conditions' => [
+ 0 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnProperty',
+ 'args' => [
+ 0 => 'firefly.feature-flags.enabled',
+ 1 => 'true',
+ 2 => false,
+ ],
+ ],
+ ],
+ 'beanConditions' => [
+ ],
+ ],
+ 2 => [
+ 'class' => 'Firefly\\FeatureFlags\\Evaluation\\FlagdEvaluatorConfiguration',
+ 'postConstruct' => [
+ ],
+ 'preDestroy' => [
+ ],
+ 'listeners' => [
+ ],
+ 'conditions' => [
+ ],
+ 'beanConditions' => [
+ 0 => [
+ 'method' => 'flagdEvaluator',
+ 'conditions' => [
+ 0 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnProperty',
+ 'args' => [
+ 0 => 'firefly.feature-flags.enabled',
+ 1 => 'true',
+ 2 => false,
+ ],
+ ],
+ 1 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnMissingBean',
+ 'args' => [
+ 0 => 'Firefly\\FeatureFlags\\Evaluation\\FlagdEvaluator',
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ 3 => [
+ 'class' => 'Firefly\\FeatureFlags\\FeatureFlagsAutoConfiguration',
+ 'postConstruct' => [
+ ],
+ 'preDestroy' => [
+ ],
+ 'listeners' => [
+ ],
+ 'conditions' => [
+ ],
+ 'beanConditions' => [
+ 0 => [
+ 'method' => 'featureFlagsSettings',
+ 'conditions' => [
+ 0 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnProperty',
+ 'args' => [
+ 0 => 'firefly.feature-flags.enabled',
+ 1 => 'true',
+ 2 => false,
+ ],
+ ],
+ 1 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnMissingBean',
+ 'args' => [
+ 0 => 'Firefly\\FeatureFlags\\FeatureFlagsSettings',
+ ],
+ ],
+ ],
+ ],
+ 1 => [
+ 'method' => 'featureFlagMetrics',
+ 'conditions' => [
+ 0 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnProperty',
+ 'args' => [
+ 0 => 'firefly.feature-flags.enabled',
+ 1 => 'true',
+ 2 => false,
+ ],
+ ],
+ 1 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnMissingBean',
+ 'args' => [
+ 0 => 'Firefly\\FeatureFlags\\Telemetry\\FeatureFlagMetrics',
+ ],
+ ],
+ ],
+ ],
+ 2 => [
+ 'method' => 'flagRegistry',
+ 'conditions' => [
+ 0 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnProperty',
+ 'args' => [
+ 0 => 'firefly.feature-flags.enabled',
+ 1 => 'true',
+ 2 => false,
+ ],
+ ],
+ 1 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnMissingBean',
+ 'args' => [
+ 0 => 'OpenFeature\\interfaces\\provider\\Provider',
+ ],
+ ],
+ 2 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnMissingBean',
+ 'args' => [
+ 0 => 'Firefly\\FeatureFlags\\Registry\\FlagRegistry',
+ ],
+ ],
+ ],
+ ],
+ 3 => [
+ 'method' => 'openFeatureProvider',
+ 'conditions' => [
+ 0 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnProperty',
+ 'args' => [
+ 0 => 'firefly.feature-flags.enabled',
+ 1 => 'true',
+ 2 => false,
+ ],
+ ],
+ 1 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnMissingBean',
+ 'args' => [
+ 0 => 'OpenFeature\\interfaces\\provider\\Provider',
+ ],
+ ],
+ ],
+ ],
+ 4 => [
+ 'method' => 'evaluationContextResolver',
+ 'conditions' => [
+ 0 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnProperty',
+ 'args' => [
+ 0 => 'firefly.feature-flags.enabled',
+ 1 => 'true',
+ 2 => false,
+ ],
+ ],
+ 1 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnMissingBean',
+ 'args' => [
+ 0 => 'Firefly\\FeatureFlags\\Context\\EvaluationContextResolver',
+ ],
+ ],
+ ],
+ ],
+ 5 => [
+ 'method' => 'featureFlags',
+ 'conditions' => [
+ 0 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnProperty',
+ 'args' => [
+ 0 => 'firefly.feature-flags.enabled',
+ 1 => 'true',
+ 2 => false,
+ ],
+ ],
+ 1 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnMissingBean',
+ 'args' => [
+ 0 => 'Firefly\\FeatureFlags\\FeatureFlags',
+ ],
+ ],
+ ],
+ ],
+ 6 => [
+ 'method' => 'featureFlagsClient',
+ 'conditions' => [
+ 0 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnProperty',
+ 'args' => [
+ 0 => 'firefly.feature-flags.enabled',
+ 1 => 'true',
+ 2 => false,
+ ],
+ ],
+ 1 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnMissingBean',
+ 'args' => [
+ 0 => 'OpenFeature\\interfaces\\flags\\Client',
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ 4 => [
+ 'class' => 'Firefly\\FeatureFlags\\Gating\\FeatureFlagsGatingConfiguration',
+ 'postConstruct' => [
+ ],
+ 'preDestroy' => [
+ ],
+ 'listeners' => [
+ ],
+ 'conditions' => [
+ ],
+ 'beanConditions' => [
+ 0 => [
+ 'method' => 'featureFlagGate',
+ 'conditions' => [
+ 0 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnMissingBean',
+ 'args' => [
+ 0 => 'Firefly\\FeatureFlags\\Gating\\FeatureFlagGate',
+ ],
+ ],
+ ],
+ ],
+ 1 => [
+ 'method' => 'routeGateDecisions',
+ 'conditions' => [
+ 0 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnMissingBean',
+ 'args' => [
+ 0 => 'Firefly\\FeatureFlags\\Gating\\RouteGateDecisions',
+ ],
+ ],
+ ],
+ ],
+ 2 => [
+ 'method' => 'featureFlagMethodInterceptor',
+ 'conditions' => [
+ 0 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnMissingBean',
+ 'args' => [
+ 0 => 'Firefly\\FeatureFlags\\Gating\\FeatureFlagMethodInterceptor',
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ 5 => [
+ 'class' => 'Firefly\\FeatureFlags\\Management\\FeatureFlagsManagementConfiguration',
+ 'postConstruct' => [
+ ],
+ 'preDestroy' => [
+ ],
+ 'listeners' => [
+ ],
+ 'conditions' => [
+ ],
+ 'beanConditions' => [
+ 0 => [
+ 'method' => 'flagManagement',
+ 'conditions' => [
+ 0 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnProperty',
+ 'args' => [
+ 0 => 'firefly.feature-flags.enabled',
+ 1 => 'true',
+ 2 => false,
+ ],
+ ],
+ 1 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnMissingBean',
+ 'args' => [
+ 0 => 'Firefly\\FeatureFlags\\Management\\FlagManagement',
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ 6 => [
+ 'class' => 'Firefly\\FeatureFlags\\Source\\ConfigFlagSource',
+ 'postConstruct' => [
+ ],
+ 'preDestroy' => [
+ ],
+ 'listeners' => [
+ ],
+ 'conditions' => [
+ 0 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnProperty',
+ 'args' => [
+ 0 => 'firefly.feature-flags.enabled',
+ 1 => 'true',
+ 2 => false,
+ ],
+ ],
+ 1 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnMissingBean',
+ 'args' => [
+ 0 => 'OpenFeature\\interfaces\\provider\\Provider',
+ ],
+ ],
+ ],
+ 'beanConditions' => [
+ ],
+ ],
+ 7 => [
+ 'class' => 'Firefly\\FeatureFlags\\Source\\FileFlagSource',
+ 'postConstruct' => [
+ ],
+ 'preDestroy' => [
+ ],
+ 'listeners' => [
+ ],
+ 'conditions' => [
+ 0 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnProperty',
+ 'args' => [
+ 0 => 'firefly.feature-flags.enabled',
+ 1 => 'true',
+ 2 => false,
+ ],
+ ],
+ 1 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnProperty',
+ 'args' => [
+ 0 => 'firefly.feature-flags.sources.file.enabled',
+ 1 => 'true',
+ 2 => false,
+ ],
+ ],
+ 2 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnMissingBean',
+ 'args' => [
+ 0 => 'OpenFeature\\interfaces\\provider\\Provider',
+ ],
+ ],
+ ],
+ 'beanConditions' => [
+ ],
+ ],
+ 8 => [
+ 'class' => 'Firefly\\FeatureFlags\\Source\\HttpFlagSource',
+ 'postConstruct' => [
+ ],
+ 'preDestroy' => [
+ ],
+ 'listeners' => [
+ ],
+ 'conditions' => [
+ 0 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnProperty',
+ 'args' => [
+ 0 => 'firefly.feature-flags.enabled',
+ 1 => 'true',
+ 2 => false,
+ ],
+ ],
+ 1 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnProperty',
+ 'args' => [
+ 0 => 'firefly.feature-flags.sources.http.enabled',
+ 1 => 'true',
+ 2 => false,
+ ],
+ ],
+ 2 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnMissingBean',
+ 'args' => [
+ 0 => 'OpenFeature\\interfaces\\provider\\Provider',
+ ],
+ ],
+ ],
+ 'beanConditions' => [
+ ],
+ ],
+ 9 => [
+ 'class' => 'Firefly\\FeatureFlags\\Source\\StoreFlagSource',
+ 'postConstruct' => [
+ ],
+ 'preDestroy' => [
+ ],
+ 'listeners' => [
+ ],
+ 'conditions' => [
+ 0 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnProperty',
+ 'args' => [
+ 0 => 'firefly.feature-flags.enabled',
+ 1 => 'true',
+ 2 => false,
+ ],
+ ],
+ 1 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnProperty',
+ 'args' => [
+ 0 => 'firefly.feature-flags.sources.store.enabled',
+ 1 => 'true',
+ 2 => false,
+ ],
+ ],
+ 2 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnMissingBean',
+ 'args' => [
+ 0 => 'OpenFeature\\interfaces\\provider\\Provider',
+ ],
+ ],
+ ],
+ 'beanConditions' => [
+ ],
+ ],
+ 10 => [
+ 'class' => 'Firefly\\FeatureFlags\\Store\\FeatureFlagStoreConfiguration',
+ 'postConstruct' => [
+ ],
+ 'preDestroy' => [
+ ],
+ 'listeners' => [
+ ],
+ 'conditions' => [
+ ],
+ 'beanConditions' => [
+ 0 => [
+ 'method' => 'flagStore',
+ 'conditions' => [
+ 0 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnProperty',
+ 'args' => [
+ 0 => 'firefly.feature-flags.enabled',
+ 1 => 'true',
+ 2 => false,
+ ],
+ ],
+ 1 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnProperty',
+ 'args' => [
+ 0 => 'firefly.feature-flags.sources.store.enabled',
+ 1 => 'true',
+ 2 => false,
+ ],
+ ],
+ 2 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnBean',
+ 'args' => [
+ 0 => 'Firefly\\FeatureFlags\\Registry\\FlagRegistry',
+ ],
+ ],
+ 3 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnMissingBean',
+ 'args' => [
+ 0 => 'Firefly\\FeatureFlags\\Store\\FlagStore',
+ ],
+ ],
+ ],
+ ],
+ 1 => [
+ 'method' => 'flagStoreWriter',
+ 'conditions' => [
+ 0 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnProperty',
+ 'args' => [
+ 0 => 'firefly.feature-flags.enabled',
+ 1 => 'true',
+ 2 => false,
+ ],
+ ],
+ 1 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnProperty',
+ 'args' => [
+ 0 => 'firefly.feature-flags.sources.store.enabled',
+ 1 => 'true',
+ 2 => false,
+ ],
+ ],
+ 2 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnBean',
+ 'args' => [
+ 0 => 'Firefly\\FeatureFlags\\Registry\\FlagRegistry',
+ ],
+ ],
+ 3 => [
+ 'type' => 'Firefly\\Context\\Condition\\Attributes\\ConditionalOnMissingBean',
+ 'args' => [
+ 0 => 'Firefly\\FeatureFlags\\Store\\FlagStoreWriter',
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+];
diff --git a/packages/feature-flags/composer.json b/packages/feature-flags/composer.json
new file mode 100644
index 00000000..4bc031f8
--- /dev/null
+++ b/packages/feature-flags/composer.json
@@ -0,0 +1,73 @@
+{
+ "name": "firefly/feature-flags",
+ "description": "LaraFly feature flags on OpenFeature: flagd documents evaluated in-process, layered config/file/http/store sources, #[FeatureFlag] gating, a writable store with an audit trail and a sync endpoint.",
+ "type": "library",
+ "license": "Apache-2.0",
+ "homepage": "https://github.com/fireflyframework/fireflyframework-php",
+ "authors": [
+ {
+ "name": "Firefly Software Solutions Inc.",
+ "homepage": "https://github.com/fireflyframework"
+ }
+ ],
+ "keywords": [
+ "firefly",
+ "laravel",
+ "feature-flags",
+ "openfeature",
+ "flagd"
+ ],
+ "support": {
+ "issues": "https://github.com/fireflyframework/fireflyframework-php/issues",
+ "source": "https://github.com/fireflyframework/fireflyframework-php/tree/main/packages/feature-flags"
+ },
+ "require": {
+ "php": "^8.3",
+ "firefly/actuator": "*@dev",
+ "firefly/autoconfigure": "*@dev",
+ "firefly/config": "*@dev",
+ "firefly/container": "*@dev",
+ "firefly/context": "*@dev",
+ "firefly/data": "*@dev",
+ "firefly/kernel": "*@dev",
+ "firefly/resilience": "*@dev",
+ "firefly/web": "*@dev",
+ "illuminate/cache": "^13.0",
+ "illuminate/console": "^13.0",
+ "illuminate/container": "^13.0",
+ "illuminate/contracts": "^13.0",
+ "illuminate/database": "^13.0",
+ "illuminate/events": "^13.0",
+ "illuminate/http": "^13.0",
+ "illuminate/routing": "^13.0",
+ "illuminate/support": "^13.0",
+ "open-feature/sdk": "^2.3",
+ "psr/log": "^3.0",
+ "symfony/yaml": "^7.4|^8.0"
+ },
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Firefly\\FeatureFlags\\FeatureFlagsServiceProvider",
+ "Firefly\\FeatureFlags\\FeatureFlagsWiringProvider"
+ ]
+ },
+ "branch-alias": {
+ "dev-main": "26.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Firefly\\FeatureFlags\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Firefly\\FeatureFlags\\Tests\\": "tests/"
+ }
+ },
+ "minimum-stability": "stable",
+ "config": {
+ "sort-packages": true
+ }
+}
diff --git a/packages/feature-flags/database/migrations/2026_10_01_000000_create_firefly_feature_flags_tables.php b/packages/feature-flags/database/migrations/2026_10_01_000000_create_firefly_feature_flags_tables.php
new file mode 100644
index 00000000..7f0eb938
--- /dev/null
+++ b/packages/feature-flags/database/migrations/2026_10_01_000000_create_firefly_feature_flags_tables.php
@@ -0,0 +1,31 @@
+getConnection()));
+ }
+
+ public function down(): void
+ {
+ FeatureFlagSchema::drop(Schema::connection($this->getConnection()));
+ }
+};
diff --git a/packages/feature-flags/src/Boot/FeatureFlagsLifecycle.php b/packages/feature-flags/src/Boot/FeatureFlagsLifecycle.php
new file mode 100644
index 00000000..18b81ffd
--- /dev/null
+++ b/packages/feature-flags/src/Boot/FeatureFlagsLifecycle.php
@@ -0,0 +1,126 @@
+|null */
+ private static ?WeakMap $active = null;
+
+ private static int $nextSequence = 0;
+
+ private ?Provider $installed = null;
+
+ private ?Provider $previous = null;
+
+ private ?self $previousOwner = null;
+
+ private int $sequence = 0;
+
+ public function __construct(
+ private readonly FireflyContainer $beans,
+ private readonly ?API $api = null,
+ ) {}
+
+ public function start(): void
+ {
+ $provider = $this->beans->get(Provider::class);
+ if ($provider instanceof Provider) {
+ $api = $this->api();
+ $this->previous = $api->getProvider();
+ $this->previousOwner = $this->activeOwner($this->previous);
+ $this->installed = $provider;
+ $this->sequence = ++self::$nextSequence;
+ self::$active ??= new WeakMap;
+ self::$active[$this] = true;
+ }
+
+ try {
+ if ($this->beans->has(FlagRegistry::class)) {
+ $registry = $this->beans->get(FlagRegistry::class);
+ if ($registry instanceof FlagRegistry) {
+ $registry->start();
+ }
+ }
+
+ if ($provider instanceof Provider) {
+ $this->api()->setProvider($provider);
+ }
+ } catch (Throwable $failure) {
+ $this->stop();
+ throw $failure;
+ }
+ }
+
+ public function stop(): void
+ {
+ if ($this->installed !== null) {
+ if ($this->ownsCurrentProvider() && $this->previous !== null) {
+ $this->api()->setProvider($this->previous);
+ }
+
+ // A later application may still be running. Link its restoration past this stopped installation.
+ foreach (self::$active ?? [] as $lifecycle => $_) {
+ if ($lifecycle->previousOwner === $this) {
+ $lifecycle->previous = $this->previous;
+ $lifecycle->previousOwner = $this->previousOwner;
+ }
+ }
+ }
+
+ if (self::$active !== null) {
+ unset(self::$active[$this]);
+ }
+ $this->installed = null;
+ $this->previous = null;
+ $this->previousOwner = null;
+ }
+
+ private function api(): API
+ {
+ return $this->api ?? OpenFeatureAPI::getInstance();
+ }
+
+ private function ownsCurrentProvider(): bool
+ {
+ if ($this->api()->getProvider() !== $this->installed) {
+ return false;
+ }
+
+ foreach (self::$active ?? [] as $lifecycle => $_) {
+ if ($lifecycle !== $this && $lifecycle->api() === $this->api() && $lifecycle->installed === $this->installed && $lifecycle->sequence > $this->sequence) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private function activeOwner(Provider $provider): ?self
+ {
+ $owner = null;
+ foreach (self::$active ?? [] as $lifecycle => $_) {
+ if ($lifecycle->api() === $this->api() && $lifecycle->installed === $provider && ($owner === null || $lifecycle->sequence > $owner->sequence)) {
+ $owner = $lifecycle;
+ }
+ }
+
+ return $owner;
+ }
+}
diff --git a/packages/feature-flags/src/Console/FlagsCommand.php b/packages/feature-flags/src/Console/FlagsCommand.php
new file mode 100644
index 00000000..c6df4b62
--- /dev/null
+++ b/packages/feature-flags/src/Console/FlagsCommand.php
@@ -0,0 +1,197 @@
+ */
+ private const array ACTIONS = ['list', 'show', 'evaluate', 'enable', 'disable', 'default-variant', 'put', 'delete'];
+
+ protected $signature = 'firefly:flags
+ {action : list, show, evaluate, enable, disable, default-variant, put or delete}
+ {key? : The flag key (every action but list)}
+ {value? : The variant, for default-variant}
+ {--file= : put: a JSON file holding one flagd flag definition}
+ {--context= : evaluate: the evaluation context, a JSON object}
+ {--targeting-key= : evaluate: the targeting key}
+ {--expected-version= : writes: require this stored version (0 means absent)}
+ {--json : Print the response body as JSON}';
+
+ protected $description = 'List, inspect, evaluate and change feature flags.';
+
+ public function handle(FireflyContainer $beans): int
+ {
+ try {
+ $management = $beans->has(FlagManagement::class) ? $beans->get(FlagManagement::class) : null;
+ if (! $management instanceof FlagManagement) {
+ throw new FlagManagementException(ManagementError::BadRequest, 'Feature flags are switched off: set firefly.feature-flags.enabled to true.');
+ }
+
+ $action = $this->stringArgument('action') ?? '';
+ if (! in_array($action, self::ACTIONS, true)) {
+ throw new FlagManagementException(ManagementError::BadRequest, "Unknown action [{$action}]: use one of ".implode(', ', self::ACTIONS).'.');
+ }
+
+ if ($action === 'list') {
+ return $this->render($management->overview());
+ }
+
+ $key = $this->stringArgument('key') ?? throw new FlagManagementException(ManagementError::BadRequest, "{$action} needs a flag key.");
+ $result = $action === 'show'
+ ? $management->describe($key)
+ : $management->apply($key, $this->body($action), self::actor());
+
+ return $this->render($result);
+ } catch (FlagManagementException $refused) {
+ if ($this->option('json') === true) {
+ $this->line(Json::encode($refused->toArray()));
+ } else {
+ $this->components->error("{$refused->error->value}: {$refused->getMessage()}");
+ }
+
+ return self::FAILURE;
+ }
+ }
+
+ /** @return array */
+ private function body(string $action): array
+ {
+ $body = ['action' => $action];
+
+ $expected = $this->option('expected-version');
+ if (is_string($expected)) {
+ $digits = ltrim($expected, '0');
+ $limit = (string) PHP_INT_MAX;
+ if (preg_match('/^\d+$/D', $expected) !== 1 || strlen($digits) > strlen($limit)
+ || (strlen($digits) === strlen($limit) && strcmp($digits, $limit) > 0)) {
+ throw new FlagManagementException(ManagementError::BadRequest, '--expected-version must be a non-negative integer within the PHP integer range.');
+ }
+ $body['expectedVersion'] = (int) $expected;
+ }
+
+ if ($action === 'default-variant') {
+ $body['variant'] = $this->stringArgument('value') ?? throw new FlagManagementException(ManagementError::BadRequest, 'default-variant needs the variant: firefly:flags default-variant {key} {variant}.');
+ }
+
+ if ($action === 'put') {
+ $file = $this->stringOption('file') ?? throw new FlagManagementException(ManagementError::BadRequest, 'put needs --file=.');
+ $contents = is_file($file) && is_readable($file) ? @file_get_contents($file) : false;
+ if ($contents === false) {
+ throw new FlagManagementException(ManagementError::BadRequest, "Cannot read [{$file}].");
+ }
+ $definition = self::decode($contents, "[{$file}] is not valid JSON.");
+ if (! Json::isObject($definition)) {
+ throw new FlagManagementException(ManagementError::BadRequest, "[{$file}] must contain a JSON object.");
+ }
+ $body['definition'] = $definition;
+ }
+
+ if ($action === 'evaluate') {
+ $context = $this->option('context');
+ if (is_string($context)) {
+ $decoded = self::decode($context, '--context must be a JSON object.');
+ if (! Json::isObject($decoded)) {
+ throw new FlagManagementException(ManagementError::BadRequest, '--context must be a JSON object.');
+ }
+ $body['context'] = $decoded;
+ }
+ $targetingKey = $this->stringOption('targeting-key');
+ if ($targetingKey !== null) {
+ $body['targetingKey'] = $targetingKey;
+ }
+ }
+
+ return $body;
+ }
+
+ /** @param array $body */
+ private function render(array $body): int
+ {
+ if ($this->option('json') === true) {
+ $this->line(Json::encode($body));
+
+ return self::SUCCESS;
+ }
+
+ if (isset($body['flags']) && is_array($body['flags'])) {
+ $rows = [];
+ foreach ($body['flags'] as $flag) {
+ if (is_array($flag)) {
+ $rows[] = [
+ self::cell($flag['key'] ?? ''),
+ self::cell($flag['state'] ?? ''),
+ self::cell($flag['type'] ?? ''),
+ self::cell($flag['defaultVariant'] ?? ''),
+ self::cell($flag['origin'] ?? ''),
+ ($flag['expired'] ?? false) === true ? 'yes' : '',
+ ];
+ }
+ }
+ $this->table(['Key', 'State', 'Type', 'Default', 'Origin', 'Expired'], $rows);
+
+ return self::SUCCESS;
+ }
+
+ if (($body['refreshPending'] ?? false) === true) {
+ $this->components->info('Write accepted for ['.self::cell($body['key'] ?? '').']; local visibility is pending. Check again after the next refresh.');
+
+ return self::SUCCESS;
+ }
+
+ if (($body['deleted'] ?? false) === true) {
+ $this->components->info('Deleted ['.self::cell($body['key'] ?? '').'].');
+
+ return self::SUCCESS;
+ }
+
+ $this->line(json_encode($body, JSON_PRETTY_PRINT | Json::ENCODE_FLAGS | JSON_THROW_ON_ERROR));
+
+ return self::SUCCESS;
+ }
+
+ private static function decode(string $json, string $refusal): mixed
+ {
+ try {
+ return Json::decode($json);
+ } catch (JsonException) {
+ throw new FlagManagementException(ManagementError::BadRequest, $refusal);
+ }
+ }
+
+ private static function actor(): string
+ {
+ $entry = function_exists('posix_geteuid') && function_exists('posix_getpwuid') ? posix_getpwuid(posix_geteuid()) : false;
+ $user = is_array($entry) && $entry['name'] !== '' ? $entry['name'] : null;
+
+ return 'cli:'.($user ?? (getenv('USER') ?: getenv('USERNAME') ?: 'unknown'));
+ }
+
+ private static function cell(mixed $value): string
+ {
+ return is_scalar($value) ? (string) $value : '';
+ }
+
+ private function stringArgument(string $name): ?string
+ {
+ $value = $this->argument($name);
+
+ return is_string($value) && $value !== '' ? $value : null;
+ }
+
+ private function stringOption(string $name): ?string
+ {
+ $value = $this->option($name);
+
+ return is_string($value) && $value !== '' ? $value : null;
+ }
+}
diff --git a/packages/feature-flags/src/Context/ApplicationEvaluationContextContributor.php b/packages/feature-flags/src/Context/ApplicationEvaluationContextContributor.php
new file mode 100644
index 00000000..6c72650c
--- /dev/null
+++ b/packages/feature-flags/src/Context/ApplicationEvaluationContextContributor.php
@@ -0,0 +1,44 @@
+|null */
+ private ?array $profiles = null;
+
+ public function __construct(private readonly Repository $config) {}
+
+ public function contribute(EvaluationContextBuilder $context): void
+ {
+ if ($this->profiles === null) {
+ $name = $this->config->get('app.name');
+ $this->application = is_string($name) && $name !== '' ? $name : null;
+ $this->profiles = (new ProfileResolver($this->config))->resolve()->all();
+ }
+
+ if ($this->application !== null) {
+ $context->set('application', $this->application);
+ }
+ $context->set('profiles', $this->profiles);
+ }
+}
diff --git a/packages/feature-flags/src/Context/EvaluationContextBuilder.php b/packages/feature-flags/src/Context/EvaluationContextBuilder.php
new file mode 100644
index 00000000..955053fe
--- /dev/null
+++ b/packages/feature-flags/src/Context/EvaluationContextBuilder.php
@@ -0,0 +1,64 @@
+ */
+ private array $attributes = [];
+
+ public function targetingKey(): ?string
+ {
+ return $this->targetingKey;
+ }
+
+ /** An empty key is no key (anonymous traffic). */
+ public function setTargetingKey(?string $targetingKey): self
+ {
+ $this->targetingKey = $targetingKey === '' ? null : $targetingKey;
+
+ return $this;
+ }
+
+ public function has(string $name): bool
+ {
+ return array_key_exists($name, $this->attributes);
+ }
+
+ public function get(string $name): mixed
+ {
+ return $this->attributes[$name] ?? null;
+ }
+
+ public function set(string $name, mixed $value): self
+ {
+ $this->attributes[$name] = $value;
+
+ return $this;
+ }
+
+ public function remove(string $name): self
+ {
+ unset($this->attributes[$name]);
+
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function attributes(): array
+ {
+ return $this->attributes;
+ }
+}
diff --git a/packages/feature-flags/src/Context/EvaluationContextContributor.php b/packages/feature-flags/src/Context/EvaluationContextContributor.php
new file mode 100644
index 00000000..86ee9cc5
--- /dev/null
+++ b/packages/feature-flags/src/Context/EvaluationContextContributor.php
@@ -0,0 +1,22 @@
+ $user->id]`,
+ * read as `"42"`) or a Stringable such as a UUID. Any other type is refused with a DEBUG line naming it, and the
+ * ambient key applies — and so is an Eloquent model or a collection (Arrayable/Jsonable): both are Stringable, but
+ * as their JSON text, which would key a rollout on every attribute the model holds (pass `$user->id`). A
+ * contributor that throws is skipped (logged at DEBUG): context is best-effort, an evaluation must not fail.
+ *
+ * Not ambient (`ambient: false`, the management preview): only the PROCESS attributes — what the
+ * ApplicationEvaluationContextContributor gives, `application` and `profiles` — under the explicit context. No
+ * other contributor runs, so neither the caller's principal, roles nor tenant can reach the evaluation.
+ *
+ * Top-level attributes are fitted to what OpenFeature's Attributes carry through the SDK client — null, scalars,
+ * arrays and DateTime; any other value makes the SDK fail the whole evaluation, and a numeric-looking name (an int
+ * key in PHP) makes its attribute merge, which every client evaluation runs, drop every attribute (first name) or
+ * fail (a later one):
+ *
+ * - a DateTimeImmutable becomes the equal DateTime (same instant, zone and microseconds); date-times are not
+ * converted further here — the evaluator turns them into epoch milliseconds;
+ * - a Stringable becomes its string — except an Eloquent model or a collection (Arrayable/Jsonable), whose string
+ * is its JSON text: it is dropped (pass the values a rule reads, such as `$user->plan`);
+ * - a JSON object (stdClass) becomes its members when they form Json's faithful array (non-empty, not a list).
+ * `{}` and a list-like object (`{"0": …}`) have no such form at the top level — as arrays they would read as
+ * lists, a different JSON value — so they are dropped rather than altered;
+ * - a numeric-looking name, an enum, any other object or a resource is dropped.
+ *
+ * Every dropped attribute is logged at DEBUG, once per evaluation, with its name and the reason. Values INSIDE an
+ * array are left exactly as they are: the SDK carries arrays as `mixed[]` without looking in, and the evaluator
+ * reads Json's faithful form (a nested stdClass is a JSON object there), so a nested `{}` keeps its meaning.
+ */
+final class EvaluationContextResolver
+{
+ /**
+ * @param list $contributors in #[Order] order
+ */
+ public function __construct(
+ private readonly array $contributors = [],
+ private readonly LoggerInterface $logger = new NullLogger,
+ ) {}
+
+ /** Every contributor, in order. */
+ public function ambient(): EvaluationContextBuilder
+ {
+ return $this->contribute($this->contributors);
+ }
+
+ /** The process attributes only: the ApplicationEvaluationContextContributor's `application` and `profiles`. */
+ public function process(): EvaluationContextBuilder
+ {
+ return $this->contribute(array_values(array_filter(
+ $this->contributors,
+ static fn (EvaluationContextContributor $contributor): bool => $contributor instanceof ApplicationEvaluationContextContributor,
+ )));
+ }
+
+ /**
+ * @param array $explicit attribute name => value; `targetingKey` names the targeting key
+ */
+ public function resolve(array $explicit = [], ?string $targetingKey = null, bool $ambient = true): EvaluationContext
+ {
+ $base = $ambient ? $this->ambient() : $this->process();
+ $explicitKey = array_key_exists('targetingKey', $explicit) ? $this->explicitKey($explicit['targetingKey']) : null;
+ unset($explicit['targetingKey']);
+
+ $key = match (true) {
+ $targetingKey !== null && $targetingKey !== '' => $targetingKey,
+ $explicitKey !== null => $explicitKey,
+ default => $base->targetingKey(),
+ };
+
+ $attributes = [];
+ foreach (array_replace($base->attributes(), $explicit) as $name => $value) {
+ if (! is_string($name)) {
+ $this->dropped((string) $name, 'its name is numeric-looking (an int key in PHP), which OpenFeature cannot carry');
+
+ continue;
+ }
+
+ $this->carry($attributes, $name, $value);
+ }
+
+ return new EvaluationContext($key, new Attributes($attributes));
+ }
+
+ /** The explicit targeting key as text: a non-empty string, an int or a Stringable; null (logged) otherwise. */
+ private function explicitKey(mixed $key): ?string
+ {
+ if ($key === null || $key === '') {
+ return null;
+ }
+
+ if (is_string($key)) {
+ return $key;
+ }
+
+ if (is_int($key)) {
+ return (string) $key;
+ }
+
+ if ($key instanceof Arrayable || $key instanceof Jsonable) {
+ return $this->refusedKey($key, 'an Eloquent model or a collection is not read as its JSON text: pass $user->id');
+ }
+
+ if ($key instanceof Stringable) {
+ try {
+ $text = (string) $key;
+ } catch (Throwable $failure) {
+ return $this->refusedKey($key, 'its string conversion failed: '.$failure->getMessage());
+ }
+
+ return $text === '' ? null : $text;
+ }
+
+ return $this->refusedKey($key, 'a targeting key is a string, an int or a Stringable');
+ }
+
+ private function refusedKey(mixed $key, string $reason): null
+ {
+ $this->logger->debug('The explicit evaluation-context targetingKey of type {type} was refused ({reason}); the ambient key, if any, applies.', [
+ 'type' => get_debug_type($key),
+ 'reason' => $reason,
+ ]);
+
+ return null;
+ }
+
+ /**
+ * Puts the value OpenFeature carries for top-level attribute $name into $attributes, or logs why there is none.
+ *
+ * @param array|null> $attributes
+ */
+ private function carry(array &$attributes, string $name, mixed $value): void
+ {
+ if ($value === null || is_bool($value) || is_int($value) || is_float($value) || is_string($value) || is_array($value) || $value instanceof DateTime) {
+ $attributes[$name] = $value;
+
+ return;
+ }
+
+ if ($value instanceof DateTimeImmutable) {
+ $attributes[$name] = DateTime::createFromImmutable($value);
+
+ return;
+ }
+
+ if ($value instanceof Arrayable || $value instanceof Jsonable) {
+ $this->dropped($name, 'an Eloquent model or a collection ('.get_debug_type($value).'), which is not read as its JSON text: pass the values a rule reads, such as $user->plan');
+
+ return;
+ }
+
+ if ($value instanceof Stringable) {
+ try {
+ $attributes[$name] = (string) $value;
+ } catch (Throwable $failure) {
+ $this->dropped($name, 'its string conversion failed: '.$failure->getMessage());
+ }
+
+ return;
+ }
+
+ if ($value instanceof stdClass) {
+ $members = get_object_vars($value);
+ if ($members !== [] && ! array_is_list($members)) {
+ $attributes[$name] = $members;
+ } else {
+ $this->dropped($name, 'an empty or list-like JSON object ({} or {"0": …}) would read as a list once it is an array, the only top-level form OpenFeature carries');
+ }
+
+ return;
+ }
+
+ $this->dropped($name, match (true) {
+ $value instanceof UnitEnum => 'an enum ('.$value::class.'), which OpenFeature cannot carry: pass its value',
+ is_object($value) => 'an object of class '.$value::class.', which OpenFeature cannot carry: pass a JSON value, a date-time or a Stringable',
+ default => 'a value of type '.get_debug_type($value).', which OpenFeature cannot carry',
+ });
+ }
+
+ private function dropped(string $name, string $reason): void
+ {
+ $this->logger->debug('Evaluation-context attribute [{name}] was dropped: {reason}.', ['name' => $name, 'reason' => $reason]);
+ }
+
+ /**
+ * @param list $contributors
+ */
+ private function contribute(array $contributors): EvaluationContextBuilder
+ {
+ $context = new EvaluationContextBuilder;
+ foreach ($contributors as $contributor) {
+ try {
+ $contributor->contribute($context);
+ } catch (Throwable $failure) {
+ $this->logger->debug('Evaluation-context contributor [{contributor}] failed and was skipped: {error}', [
+ 'contributor' => $contributor::class,
+ 'error' => $failure->getMessage(),
+ ]);
+ }
+ }
+
+ return $context;
+ }
+}
diff --git a/packages/feature-flags/src/Definition/FlagDefinition.php b/packages/feature-flags/src/Definition/FlagDefinition.php
new file mode 100644
index 00000000..3754bdcd
--- /dev/null
+++ b/packages/feature-flags/src/Definition/FlagDefinition.php
@@ -0,0 +1,176 @@
+ $raw the flagd flag object in Json's faithful form
+ */
+ public function __construct(public string $key, public array $raw) {}
+
+ public static function fromJsonValue(string $key, mixed $value): self
+ {
+ return new self($key, Json::members($value));
+ }
+
+ /** '' when absent. */
+ public function state(): string
+ {
+ $state = $this->raw['state'] ?? null;
+
+ return is_string($state) ? $state : '';
+ }
+
+ public function isDisabled(): bool
+ {
+ return $this->state() === 'DISABLED';
+ }
+
+ /**
+ * Variant name => value. A numeric-looking name (`"1"`) is an int key here; variantNames() gives it as text.
+ *
+ * @return array
+ */
+ public function variants(): array
+ {
+ return Json::members($this->raw['variants'] ?? null);
+ }
+
+ /**
+ * @return list
+ */
+ public function variantNames(): array
+ {
+ return array_map(static fn (int|string $name): string => (string) $name, array_keys($this->variants()));
+ }
+
+ public function hasVariant(string $name): bool
+ {
+ return array_key_exists($name, $this->variants());
+ }
+
+ public function variantValue(string $name): mixed
+ {
+ return $this->variants()[$name] ?? null;
+ }
+
+ /** A boolean names the variant `'true'`/`'false'`; `''` or null is no default variant. */
+ public function defaultVariant(): ?string
+ {
+ $default = $this->raw['defaultVariant'] ?? null;
+ if (is_bool($default)) {
+ return $default ? 'true' : 'false';
+ }
+
+ return is_string($default) && $default !== '' ? $default : null;
+ }
+
+ /** null when absent, null or an empty object. */
+ public function targeting(): mixed
+ {
+ $targeting = $this->raw['targeting'] ?? null;
+
+ if ($targeting === null || $targeting === [] || ($targeting instanceof stdClass && get_object_vars($targeting) === [])) {
+ return null;
+ }
+
+ return $targeting;
+ }
+
+ /**
+ * The scalar metadata entries. A numeric-looking name (`"1"`, `"2024"`) is an int key: PHP arrays cannot hold
+ * it as a string. Read a name with `(string) $name`.
+ *
+ * @return array
+ */
+ public function metadata(): array
+ {
+ $metadata = [];
+ foreach (Json::members($this->raw['metadata'] ?? null) as $name => $value) {
+ if (is_bool($value) || is_int($value) || is_float($value) || is_string($value)) {
+ $metadata[$name] = $value;
+ }
+ }
+
+ return $metadata;
+ }
+
+ /** 'boolean', 'string', 'number' or 'object', read off the first variant. */
+ public function valueType(): string
+ {
+ $values = array_values($this->variants());
+ $first = $values[0] ?? null;
+
+ return match (true) {
+ is_bool($first) => 'boolean',
+ is_int($first), is_float($first) => 'number',
+ is_string($first) => 'string',
+ default => 'object',
+ };
+ }
+
+ /** A number flag is Integer when every variant is an int, else Float. */
+ public function flagType(): FlagType
+ {
+ return match ($this->valueType()) {
+ 'boolean' => FlagType::Boolean,
+ 'string' => FlagType::String,
+ 'number' => array_filter($this->variants(), static fn (mixed $value): bool => ! is_int($value)) === [] ? FlagType::Integer : FlagType::Float,
+ default => FlagType::Object,
+ };
+ }
+
+ public function expires(): ?string
+ {
+ $expires = $this->metadata()['expires'] ?? null;
+
+ return is_string($expires) ? $expires : null;
+ }
+
+ /** `expires` is before `$today` (both YYYY-MM-DD, UTC): the flag still evaluates, it is flag debt. */
+ public function isExpired(string $today): bool
+ {
+ $expires = $this->expires();
+
+ return $expires !== null && strcmp($expires, $today) < 0;
+ }
+
+ /**
+ * @return array
+ */
+ public function toArray(): array
+ {
+ return $this->raw;
+ }
+
+ /**
+ * The definition as it must be ENCODED: the three object positions a PHP array may have flattened to `[]`
+ * (config written as PHP arrays) are forced back to objects.
+ *
+ * @return stdClass|array
+ */
+ public function toJsonValue(): stdClass|array
+ {
+ $value = $this->raw;
+ foreach (['variants', 'metadata', 'targeting'] as $position) {
+ if (array_key_exists($position, $value) && (is_array($value[$position]) || $value[$position] instanceof stdClass)) {
+ $value[$position] = Json::object($value[$position]);
+ }
+ }
+
+ return Json::object($value);
+ }
+}
diff --git a/packages/feature-flags/src/Definition/FlagDefinitions.php b/packages/feature-flags/src/Definition/FlagDefinitions.php
new file mode 100644
index 00000000..ea937d82
--- /dev/null
+++ b/packages/feature-flags/src/Definition/FlagDefinitions.php
@@ -0,0 +1,323 @@
+ */
+ public const array KINDS = ['release', 'experiment', 'ops', 'permission'];
+
+ /**
+ * The deepest a flag definition, an evaluator rule or the document metadata may nest (R-depth-validate): the
+ * definition itself is level 1, and each object or array inside adds one.
+ */
+ public const int MAX_DEPTH = 256;
+
+ /**
+ * Shorthand → flagd. Only config and test overrides accept shorthand; full definitions pass through
+ * untouched and anything else is left for validate() to refuse.
+ *
+ * @param array $flags
+ * @return array
+ */
+ public static function normalize(array $flags): array
+ {
+ $normalized = [];
+ foreach ($flags as $key => $definition) {
+ $normalized[$key] = match (true) {
+ $definition === true => ['state' => 'ENABLED', 'variants' => ['on' => true, 'off' => false], 'defaultVariant' => 'on'],
+ $definition === false => ['state' => 'ENABLED', 'variants' => ['on' => true, 'off' => false], 'defaultVariant' => 'off'],
+ is_string($definition) => ['state' => 'ENABLED', 'variants' => Json::object([$definition => $definition]), 'defaultVariant' => $definition],
+ default => $definition,
+ };
+ }
+
+ return $normalized;
+ }
+
+ /**
+ * Validate a flagd document (`flags`, `$evaluators`, `metadata`) as a whole and return it.
+ *
+ * A document that is not an object is refused with the key ``. Each section is empty when absent,
+ * null or `[]`; a present section that is not an object is refused with the section's name as the key. A single
+ * flag that breaks a rule is reported with its own key, an evaluator rule that is not an object with the key
+ * `$evaluators` (the reason names the evaluator). A `$ref` naming no evaluator is not a load error: evaluating
+ * that flag reports it.
+ *
+ * @throws InvalidFlagDefinition
+ */
+ public static function parseDocument(mixed $document): FlagDocument
+ {
+ if (! Json::isObject($document)) {
+ throw new InvalidFlagDefinition('', 'document must be an object');
+ }
+
+ $members = Json::members($document);
+
+ $definitions = [];
+ foreach (self::section($members, 'flags', 'flags must be an object') as $key => $definition) {
+ $key = (string) $key;
+ self::validate($key, $definition);
+ $definitions[$key] = FlagDefinition::fromJsonValue($key, self::emptyListsAsObjects(Json::members($definition)));
+ }
+
+ $evaluators = self::section($members, '$evaluators', '$evaluators must be an object');
+ foreach ($evaluators as $name => $rule) {
+ if (! Json::isObject($rule)) {
+ throw new InvalidFlagDefinition('$evaluators', sprintf("targeting must be an object (evaluator '%s')", $name));
+ }
+ $reason = self::walkReason($rule);
+ if ($reason !== null) {
+ throw new InvalidFlagDefinition('$evaluators', sprintf("%s (evaluator '%s')", $reason, $name));
+ }
+ }
+
+ $metadata = self::section($members, 'metadata', 'metadata must be an object');
+ $reason = self::metadataReason($metadata);
+ if ($reason !== null) {
+ throw new InvalidFlagDefinition('metadata', $reason);
+ }
+ $reason = self::walkReason($metadata);
+ if ($reason !== null) {
+ throw new InvalidFlagDefinition('metadata', $reason);
+ }
+
+ return new FlagDocument($definitions, $evaluators, $metadata);
+ }
+
+ /**
+ * @throws InvalidFlagDefinition
+ */
+ public static function validate(string $key, mixed $definition): void
+ {
+ if (preg_match(self::KEY_PATTERN, $key) !== 1) {
+ throw new InvalidFlagDefinition($key, 'invalid flag key');
+ }
+
+ if (! Json::isObject($definition)) {
+ throw new InvalidFlagDefinition($key, 'flag definition must be an object');
+ }
+
+ $members = Json::members($definition);
+
+ if (! in_array($members['state'] ?? null, ['ENABLED', 'DISABLED'], true)) {
+ throw new InvalidFlagDefinition($key, 'state must be ENABLED or DISABLED');
+ }
+
+ $variants = Json::members($members['variants'] ?? null);
+ if ($variants === []) {
+ throw new InvalidFlagDefinition($key, 'variants must be a non-empty object');
+ }
+
+ $types = array_values(array_unique(array_map(self::jsonType(...), array_values($variants))));
+ if (! in_array($types, [['boolean'], ['string'], ['number'], ['object']], true)) {
+ throw new InvalidFlagDefinition($key, 'variants must share one type');
+ }
+
+ $default = $members['defaultVariant'] ?? null;
+ if ($default !== null && (! is_string($default) || ! array_key_exists($default, $variants))) {
+ throw new InvalidFlagDefinition($key, is_bool($default)
+ ? 'defaultVariant is not a variant (a boolean names no variant: write the name as text)'
+ : 'defaultVariant is not a variant');
+ }
+
+ $targeting = $members['targeting'] ?? null;
+ if ($targeting !== null && ! Json::isObject($targeting) && $targeting !== []) {
+ throw new InvalidFlagDefinition($key, 'targeting must be an object');
+ }
+
+ $metadata = $members['metadata'] ?? null;
+ if ($metadata !== null) {
+ self::validateMetadata($key, $metadata);
+ }
+
+ $reason = self::walkReason($definition);
+ if ($reason !== null) {
+ throw new InvalidFlagDefinition($key, $reason);
+ }
+ }
+
+ /**
+ * Keys of the flags whose `expires` is before $today (YYYY-MM-DD, UTC), sorted.
+ *
+ * @param array $flags
+ * @return list
+ */
+ public static function expiredKeys(array $flags, string $today): array
+ {
+ $expired = [];
+ foreach ($flags as $flag) {
+ if ($flag->isExpired($today)) {
+ $expired[] = $flag->key;
+ }
+ }
+ sort($expired, SORT_STRING);
+
+ return $expired;
+ }
+
+ /**
+ * A validated flag's members with a `[]` targeting or metadata replaced by `{}`: the same JSON value, so the
+ * definition still encodes as written, and a reader of the raw definition meets the contract's "no targeting".
+ *
+ * @param array $members
+ * @return array
+ */
+ private static function emptyListsAsObjects(array $members): array
+ {
+ foreach (['targeting', 'metadata'] as $position) {
+ if (array_key_exists($position, $members) && $members[$position] === []) {
+ $members[$position] = new stdClass;
+ }
+ }
+
+ return $members;
+ }
+
+ /**
+ * The members of a document section: none when absent, null or `[]`.
+ *
+ * @param array $document
+ * @return array
+ *
+ * @throws InvalidFlagDefinition when the section is present and not an object
+ */
+ private static function section(array $document, string $name, string $reason): array
+ {
+ $section = $document[$name] ?? null;
+ if ($section !== null && ! Json::isObject($section) && $section !== []) {
+ throw new InvalidFlagDefinition($name, $reason);
+ }
+
+ return Json::members($section);
+ }
+
+ /**
+ * A flag's `metadata`: a map of scalars with non-empty names, and the four reserved keys well-formed.
+ *
+ * @throws InvalidFlagDefinition
+ */
+ private static function validateMetadata(string $key, mixed $metadata): void
+ {
+ $reason = Json::isObject($metadata) || $metadata === [] ? self::metadataReason(Json::members($metadata)) : 'metadata values must be scalars';
+ if ($reason !== null) {
+ throw new InvalidFlagDefinition($key, $reason);
+ }
+
+ $reserved = Json::members($metadata);
+ if (array_key_exists('kind', $reserved) && ! in_array($reserved['kind'], self::KINDS, true)) {
+ throw new InvalidFlagDefinition($key, 'kind must be one of release, experiment, ops, permission');
+ }
+ if (array_key_exists('expires', $reserved) && ! self::isDate($reserved['expires'])) {
+ throw new InvalidFlagDefinition($key, 'expires must be a YYYY-MM-DD date');
+ }
+ if (array_key_exists('owner', $reserved) && ! is_string($reserved['owner'])) {
+ throw new InvalidFlagDefinition($key, 'owner must be a string');
+ }
+ if (array_key_exists('description', $reserved) && ! is_string($reserved['description'])) {
+ throw new InvalidFlagDefinition($key, 'description must be a string');
+ }
+ }
+
+ /**
+ * Why a metadata map (a flag's or the document's) breaks the contract, or null: every value a string, number or
+ * boolean, then no empty name.
+ *
+ * @param array $metadata
+ */
+ private static function metadataReason(array $metadata): ?string
+ {
+ foreach ($metadata as $value) {
+ if (! is_bool($value) && ! is_int($value) && ! is_float($value) && ! is_string($value)) {
+ return 'metadata values must be scalars';
+ }
+ }
+
+ return array_key_exists('', $metadata) ? 'metadata keys must not be empty' : null;
+ }
+
+ /**
+ * Why $value (a flag definition, an evaluator rule or the document metadata: level 1) breaks the two rules that
+ * reach its every corner, or null. `definition nests too deeply` when an object or array sits deeper than
+ * MAX_DEPTH (reported first, whatever else the walk met); otherwise `numbers must be finite` when INF or NAN
+ * stands anywhere — JSON has neither (YAML's `.inf`/`.nan` and PHP produce them) and encoding one throws, so the
+ * definition could be neither composed, stored nor served. Walks with a stack of its own and never descends past
+ * MAX_DEPTH: no recursion, and bounded work whatever the input's depth.
+ */
+ private static function walkReason(mixed $value): ?string
+ {
+ $nonFinite = false;
+ /** @var list $pending */
+ $pending = [[$value, 1]];
+ while ($pending !== []) {
+ [$item, $level] = array_pop($pending);
+ if ($item instanceof stdClass) {
+ $item = get_object_vars($item);
+ }
+ if (is_array($item)) {
+ if ($level > self::MAX_DEPTH) {
+ return 'definition nests too deeply';
+ }
+ foreach ($item as $member) {
+ $pending[] = [$member, $level + 1];
+ }
+ } elseif (is_float($item) && ! is_finite($item)) {
+ $nonFinite = true;
+ }
+ }
+
+ return $nonFinite ? 'numbers must be finite' : null;
+ }
+
+ /** The JSON type a variant value has; null and anything JSON cannot hold share no type with a variant. */
+ private static function jsonType(mixed $value): string
+ {
+ return match (true) {
+ is_bool($value) => 'boolean',
+ is_int($value), is_float($value) => 'number',
+ is_string($value) => 'string',
+ is_array($value), $value instanceof stdClass => 'object',
+ default => get_debug_type($value),
+ };
+ }
+
+ private static function isDate(mixed $value): bool
+ {
+ return is_string($value)
+ && preg_match('/^(\d{4})-(\d{2})-(\d{2})$/D', $value, $parts) === 1
+ && checkdate((int) $parts[2], (int) $parts[3], (int) $parts[1]);
+ }
+}
diff --git a/packages/feature-flags/src/Definition/FlagDocument.php b/packages/feature-flags/src/Definition/FlagDocument.php
new file mode 100644
index 00000000..af35a1ea
--- /dev/null
+++ b/packages/feature-flags/src/Definition/FlagDocument.php
@@ -0,0 +1,113 @@
+ $flags keyed by flag key (PHP stores "2024" as int 2024: read FlagDefinition::$key)
+ * @param array $evaluators
+ * @param array $metadata as decoded: a non-scalar value is kept so validation can reject it
+ */
+ public function __construct(
+ public array $flags = [],
+ public array $evaluators = [],
+ public array $metadata = [],
+ ) {}
+
+ public static function fromJsonValue(mixed $document): self
+ {
+ $members = Json::members($document);
+
+ $flags = [];
+ foreach (Json::members($members['flags'] ?? null) as $key => $definition) {
+ $flags[$key] = FlagDefinition::fromJsonValue((string) $key, $definition);
+ }
+
+ return new self($flags, Json::members($members['$evaluators'] ?? null), Json::members($members['metadata'] ?? null));
+ }
+
+ /** @throws \JsonException */
+ public static function fromJson(string $json): self
+ {
+ return self::fromJsonValue(Json::decode($json));
+ }
+
+ public function flag(string $key): ?FlagDefinition
+ {
+ return $this->flags[$key] ?? null;
+ }
+
+ /**
+ * @return list
+ */
+ public function keys(): array
+ {
+ return array_map(static fn (FlagDefinition $flag): string => $flag->key, array_values($this->flags));
+ }
+
+ /** `{"flags":{…},"$evaluators":{…},"metadata":{…}}`, always all three, each an object even when empty. */
+ public function toJsonValue(): stdClass
+ {
+ $flags = [];
+ foreach ($this->flags as $flag) {
+ $flags[$flag->key] = $flag->toJsonValue();
+ }
+
+ return (object) [
+ 'flags' => Json::object($flags),
+ '$evaluators' => Json::object($this->evaluators),
+ 'metadata' => Json::object($this->metadata),
+ ];
+ }
+
+ /** @throws \JsonException as Json::encode() (INF or NAN in a definition) */
+ public function toJson(): string
+ {
+ return Json::encode($this->toJsonValue());
+ }
+
+ /**
+ * sha256 of the canonical form: independent of member order, sensitive to every value.
+ *
+ * @throws \JsonException as Json::encode()
+ */
+ public function fingerprint(): string
+ {
+ return hash('sha256', Json::canonical($this->toJsonValue()));
+ }
+
+ /**
+ * Flag key => sha256 of that flag's canonical definition, sorted by key as text. A numeric-looking key is an
+ * int key (PHP arrays cannot hold it as a string): read it with `(string) $key`.
+ *
+ * @return array
+ *
+ * @throws \JsonException as Json::encode()
+ */
+ public function keyFingerprints(): array
+ {
+ $fingerprints = [];
+ foreach ($this->flags as $flag) {
+ $fingerprints[$flag->key] = hash('sha256', Json::canonical($flag->toJsonValue()));
+ }
+ ksort($fingerprints, SORT_STRING);
+
+ return $fingerprints;
+ }
+}
diff --git a/packages/feature-flags/src/Definition/InvalidFlagDefinition.php b/packages/feature-flags/src/Definition/InvalidFlagDefinition.php
new file mode 100644
index 00000000..547cd885
--- /dev/null
+++ b/packages/feature-flags/src/Definition/InvalidFlagDefinition.php
@@ -0,0 +1,48 @@
+` for the document as a
+ * whole.
+ */
+final class InvalidFlagDefinition extends ConfigurationException
+{
+ public function __construct(
+ private readonly string $flagKey,
+ private readonly string $reason,
+ private readonly ?string $source = null,
+ ) {
+ parent::__construct(
+ sprintf('Invalid feature flag [%s]%s: %s.', $flagKey, $source === null ? '' : " from source [{$source}]", $reason),
+ 'INVALID_FEATURE_FLAG',
+ );
+ }
+
+ public function flagKey(): string
+ {
+ return $this->flagKey;
+ }
+
+ public function reason(): string
+ {
+ return $this->reason;
+ }
+
+ public function source(): ?string
+ {
+ return $this->source;
+ }
+
+ public function fromSource(string $source): self
+ {
+ return new self($this->flagKey, $this->reason, $source);
+ }
+}
diff --git a/packages/feature-flags/src/Definition/Json.php b/packages/feature-flags/src/Definition/Json.php
new file mode 100644
index 00000000..ee54a592
--- /dev/null
+++ b/packages/feature-flags/src/Definition/Json.php
@@ -0,0 +1,167 @@
+getCode() !== JSON_ERROR_INVALID_PROPERTY_NAME) {
+ throw $failure;
+ }
+ }
+
+ // Native validation keeps syntax, depth and number semantics. PHP objects alone cannot hold leading
+ // NUL names. Prefix every member token (not values), decode natively, then remove exactly one prefix.
+ json_decode($json, true, 512, JSON_THROW_ON_ERROR);
+ $prefixed = preg_replace_callback('/"[^"\\\\]*+(?:\\\\.[^"\\\\]*+)*+"(\s*:)?/s',
+ static fn (array $token): string => isset($token[1]) ? '"_'.substr($token[0], 1) : $token[0], $json);
+ if ($prefixed === null) {
+ throw new JsonException('Cannot tokenize validated JSON: '.preg_last_error_msg());
+ }
+
+ return self::normalizeValue(json_decode($prefixed, false, 512, JSON_THROW_ON_ERROR), true);
+ }
+
+ /**
+ * A stdClass tree (json_decode() without assoc, Yaml::PARSE_OBJECT_FOR_MAP) in the faithful form above.
+ */
+ public static function normalize(mixed $value): mixed
+ {
+ return self::normalizeValue($value, false);
+ }
+
+ private static function normalizeValue(mixed $value, bool $prefixed): mixed
+ {
+ if ($value instanceof stdClass) {
+ $members = [];
+ foreach (get_object_vars($value) as $key => $member) {
+ $members[$prefixed ? substr((string) $key, 1) : $key] = self::normalizeValue($member, $prefixed);
+ }
+
+ return $members === [] || array_is_list($members) ? (object) $members : $members;
+ }
+
+ if (is_array($value)) {
+ return array_map(static fn (mixed $member): mixed => self::normalizeValue($member, $prefixed), $value);
+ }
+
+ return $value;
+ }
+
+ /** @throws JsonException on INF or NAN, malformed UTF-8, or nesting deeper than 512 levels */
+ public static function encode(mixed $value): string
+ {
+ return json_encode($value, self::ENCODE_FLAGS | JSON_THROW_ON_ERROR);
+ }
+
+ /**
+ * The encoding with object members sorted by name, recursively; `[]` and `{}` stay distinct.
+ *
+ * @throws JsonException as encode()
+ */
+ public static function canonical(mixed $value): string
+ {
+ return self::encode(self::sorted($value));
+ }
+
+ /**
+ * A stdClass, or a non-empty array that is not a list.
+ */
+ public static function isObject(mixed $value): bool
+ {
+ return $value instanceof stdClass || (is_array($value) && $value !== [] && ! array_is_list($value));
+ }
+
+ /**
+ * An array that is a list, so `[]` is a list.
+ *
+ * @phpstan-assert-if-true list $value
+ */
+ public static function isList(mixed $value): bool
+ {
+ return is_array($value) && array_is_list($value);
+ }
+
+ /**
+ * An object's members; anything else (a list, `[]`, a scalar, null) has none.
+ *
+ * @return array
+ */
+ public static function members(mixed $value): array
+ {
+ if ($value instanceof stdClass) {
+ return get_object_vars($value);
+ }
+
+ return is_array($value) && ! array_is_list($value) ? $value : [];
+ }
+
+ /**
+ * Deep stdClass → array, for the OpenFeature SDK and PHP callers (which cannot tell `{}` from `[]`).
+ */
+ public static function toPhp(mixed $value): mixed
+ {
+ if ($value instanceof stdClass) {
+ $value = get_object_vars($value);
+ }
+
+ return is_array($value) ? array_map(self::toPhp(...), $value) : $value;
+ }
+
+ /**
+ * A value for an OBJECT position: `[]` or a list-like array becomes a stdClass so it encodes as `{…}`.
+ *
+ * @param array|stdClass $members
+ * @return array|stdClass
+ */
+ public static function object(array|stdClass $members): array|stdClass
+ {
+ if ($members instanceof stdClass) {
+ return $members;
+ }
+
+ return $members === [] || array_is_list($members) ? (object) $members : $members;
+ }
+
+ private static function sorted(mixed $value): mixed
+ {
+ if (self::isObject($value)) {
+ $members = [];
+ foreach (self::members($value) as $key => $member) {
+ $members[(string) $key] = self::sorted($member);
+ }
+ ksort($members, SORT_STRING);
+
+ return self::object($members);
+ }
+
+ return is_array($value) ? array_map(self::sorted(...), $value) : $value;
+ }
+}
diff --git a/packages/feature-flags/src/Evaluation/DefaultFlagdEvaluator.php b/packages/feature-flags/src/Evaluation/DefaultFlagdEvaluator.php
new file mode 100644
index 00000000..d21fe5b8
--- /dev/null
+++ b/packages/feature-flags/src/Evaluation/DefaultFlagdEvaluator.php
@@ -0,0 +1,354 @@
+>
+ */
+ private readonly WeakMap $expansions;
+
+ /**
+ * @param (Closure(): int)|null $clock Unix seconds for `$flagd.timestamp`
+ */
+ public function __construct(private readonly ?Closure $clock = null)
+ {
+ if (PHP_INT_SIZE < 8) {
+ throw new LogicException('Feature flag bucketing needs 64-bit integers: (hash * totalWeight) >> 32 overflows a 32-bit PHP build.');
+ }
+
+ $this->logic = FlagdOperators::jsonLogic();
+ $this->expansions = new WeakMap;
+ }
+
+ public function evaluate(FlagDocument $document, string $flagKey, FlagType $type, mixed $default, ?string $targetingKey = null, array $attributes = []): Resolution
+ {
+ try {
+ return $this->resolve($document, $flagKey, $type, $default, $targetingKey, $attributes);
+ } catch (Throwable $failure) {
+ return Resolution::error($default, EvaluationError::General, "Evaluating flag [{$flagKey}] failed: {$failure->getMessage()}");
+ }
+ }
+
+ /**
+ * @param array $attributes
+ */
+ private function resolve(FlagDocument $document, string $flagKey, FlagType $type, mixed $default, ?string $targetingKey, array $attributes): Resolution
+ {
+ $flag = $document->flag($flagKey);
+ if ($flag === null) {
+ return Resolution::error($default, EvaluationError::FlagNotFound, "Flag [{$flagKey}] is not defined.");
+ }
+
+ $metadata = array_replace(self::scalars($document->metadata), $flag->metadata());
+
+ if ($flag->isDisabled()) {
+ return self::typed(new Resolution($default, null, EvaluationReason::Disabled, metadata: $metadata), $type, $default);
+ }
+
+ $expansion = $this->expansion($document, $flagKey, $flag);
+ if ($expansion === null) {
+ return Resolution::error($default, EvaluationError::ParseError, sprintf(
+ 'The targeting of flag [%s] expands past %d JSON values or %d levels.',
+ $flagKey,
+ RefResolver::MAX_VALUES,
+ RefResolver::MAX_DEPTH,
+ ));
+ }
+ $targeting = $expansion[0];
+
+ if (! self::pythonTruthy($targeting)) {
+ return self::typed(self::fallThrough($flag, $default, $metadata, EvaluationReason::Static), $type, $default);
+ }
+
+ if (! Json::isObject($targeting)) {
+ return Resolution::error($default, EvaluationError::ParseError, "The targeting of flag [{$flagKey}] is not an object.");
+ }
+
+ try {
+ $result = $this->logic->apply($targeting, $this->data($flagKey, $targetingKey, $attributes));
+ } catch (UnknownOperator $unknown) {
+ return Resolution::error($default, EvaluationError::ParseError, "Invalid targeting for flag [{$flagKey}]: {$unknown->getMessage()}");
+ } catch (JsonLogicError $failure) {
+ return Resolution::error($default, EvaluationError::General, "Targeting for flag [{$flagKey}] failed: {$failure->getMessage()}");
+ }
+
+ if ($result === null) {
+ return self::typed(self::fallThrough($flag, $default, $metadata, EvaluationReason::Default), $type, $default);
+ }
+
+ $variant = match (true) {
+ is_bool($result) => $result ? 'true' : 'false',
+ default => PythonText::str($result),
+ };
+ if ($variant === null || ! $flag->hasVariant($variant) || $variant === '' || $flag->variantValue($variant) === null) {
+ return Resolution::error($default, EvaluationError::General, sprintf(
+ 'Targeting for flag [%s] resolved [%s], which is not one of its variants.',
+ $flagKey,
+ $variant ?? get_debug_type($result),
+ ));
+ }
+
+ return self::typed(new Resolution(self::copied($flag->variantValue($variant)), $variant, EvaluationReason::TargetingMatch, metadata: $metadata), $type, $default);
+ }
+
+ /**
+ * [the flag's targeting with its references expanded] ([null] for no targeting), or null when it expands past
+ * RefResolver's limits: worked out on the first evaluation of the flag in this document and kept with the
+ * document.
+ *
+ * @return array{0: mixed}|null
+ */
+ private function expansion(FlagDocument $document, string $flagKey, FlagDefinition $flag): ?array
+ {
+ $targeting = $flag->targeting();
+ if ($targeting === null) {
+ return [null];
+ }
+
+ $expansions = $this->expansions[$document] ?? [];
+ if (! array_key_exists($flagKey, $expansions)) {
+ $expansions[$flagKey] = RefResolver::withinLimits($targeting, $document->evaluators)
+ ? [RefResolver::expand($targeting, $document->evaluators)]
+ : null;
+ $this->expansions[$document] = $expansions;
+ }
+
+ return $expansions[$flagKey];
+ }
+
+ /**
+ * $value with every array and stdClass inside it rebuilt, in the same faithful form (`{}` stays a stdClass):
+ * the document's own objects never leave the evaluator.
+ */
+ private static function copied(mixed $value): mixed
+ {
+ if ($value instanceof stdClass) {
+ $copy = new stdClass;
+ foreach (get_object_vars($value) as $member => $item) {
+ $copy->{$member} = self::copied($item);
+ }
+
+ return $copy;
+ }
+
+ if (is_array($value)) {
+ $copy = [];
+ foreach ($value as $key => $item) {
+ $copy[$key] = self::copied($item);
+ }
+
+ return $copy;
+ }
+
+ return $value;
+ }
+
+ /**
+ * The defaultVariant's value with $reason, or the caller's default with DEFAULT when the flag names none.
+ *
+ * @param array $metadata
+ */
+ private static function fallThrough(FlagDefinition $flag, mixed $default, array $metadata, EvaluationReason $reason): Resolution
+ {
+ $variant = $flag->defaultVariant();
+ if ($variant === null) {
+ return new Resolution($default, null, EvaluationReason::Default, metadata: $metadata);
+ }
+
+ if (! $flag->hasVariant($variant)) {
+ return Resolution::error($default, EvaluationError::General, "Flag [{$flag->key}] names default variant [{$variant}], which is not one of its variants.");
+ }
+
+ return new Resolution(self::copied($flag->variantValue($variant)), $variant, $reason, metadata: $metadata);
+ }
+
+ /**
+ * The type check (FlagType::accepts(): a boolean is never a number), then a float request's number as a float.
+ * DISABLED and a DEFAULT without a variant carry the caller's default unchecked, as flagd does, and a float
+ * request gets it as float(default) there — an integer or a boolean, as Python's float() reads them. An error
+ * passes through unchanged.
+ */
+ private static function typed(Resolution $resolution, FlagType $type, mixed $default): Resolution
+ {
+ if ($resolution->error !== null) {
+ return $resolution;
+ }
+
+ $checked = $resolution->reason !== EvaluationReason::Disabled
+ && ($resolution->reason !== EvaluationReason::Default || $resolution->variant !== null);
+ $value = $resolution->value;
+ if ($checked && ! $type->accepts($value)) {
+ return Resolution::error($default, EvaluationError::TypeMismatch, sprintf('Flag resolved a %s value; a %s was requested.', get_debug_type($value), $type->value));
+ }
+
+ $numeric = is_int($value) || (! $checked && is_bool($value));
+
+ return $type === FlagType::Float && $numeric ? $resolution->withValue((float) $value) : $resolution;
+ }
+
+ /**
+ * @param array $metadata
+ * @return array
+ */
+ private static function scalars(array $metadata): array
+ {
+ return array_filter($metadata, static fn (mixed $value): bool => is_bool($value) || is_int($value) || is_float($value) || is_string($value));
+ }
+
+ /** Python's bool() of a JSON value: null, false, 0, 0.0, "", [] and {} are false. */
+ private static function pythonTruthy(mixed $value): bool
+ {
+ return match (true) {
+ $value === null, $value === false, $value === 0, $value === '', $value === [] => false,
+ is_float($value) => $value !== 0.0,
+ $value instanceof stdClass => get_object_vars($value) !== [],
+ default => true,
+ };
+ }
+
+ /**
+ * @param array $attributes
+ * @return array
+ */
+ private function data(string $flagKey, ?string $targetingKey, array $attributes): array
+ {
+ $budget = RefResolver::MAX_VALUES;
+ $changed = false;
+ $data = self::withEpochMillis($attributes, 1, $budget, $changed);
+ $data = is_array($data) ? $data : $attributes;
+ $data['$flagd'] = ['flagKey' => $flagKey, 'timestamp' => $this->clock !== null ? ($this->clock)() : time()];
+ $data['targetingKey'] = $targetingKey;
+
+ return $data;
+ }
+
+ /**
+ * PyFly's _epoch_millis_context walk: every value read spends one unit of the budget (none left: the value
+ * stays as it is); a date-time becomes its epoch milliseconds; a container past the depth limit is not
+ * entered; a container is rebuilt only when something inside it changed ($changed), else handed back as is.
+ *
+ * @param int $level the nesting level of $node (the attributes are level 1)
+ */
+ private static function withEpochMillis(mixed $node, int $level, int &$budget, bool &$changed): mixed
+ {
+ if (--$budget < 0) {
+ return $node;
+ }
+ if ($node instanceof DateTimeInterface) {
+ $changed = true;
+
+ return self::epochMillis($node);
+ }
+ if ($level > RefResolver::MAX_DEPTH || (! is_array($node) && ! $node instanceof stdClass)) {
+ return $node;
+ }
+
+ $inside = false;
+ $members = [];
+ foreach (is_array($node) ? $node : get_object_vars($node) as $key => $item) {
+ $members[$key] = self::withEpochMillis($item, $level + 1, $budget, $inside);
+ }
+ if (! $inside) {
+ return $node;
+ }
+
+ $changed = true;
+ if (is_array($node)) {
+ return $members;
+ }
+ $copy = new stdClass;
+ foreach ($members as $member => $item) {
+ $copy->{$member} = $item;
+ }
+
+ return $copy;
+ }
+
+ /**
+ * The whole microseconds since the epoch over 1000, correctly rounded: the exact decimal text, read once
+ * (PHP's text-to-float conversion rounds correctly; dividing an integer past 2^53 would round twice).
+ */
+ private static function epochMillis(DateTimeInterface $value): float
+ {
+ $seconds = (int) $value->format('U');
+ $micros = (int) $value->format('u');
+
+ if ($seconds >= 0) {
+ $sign = '';
+ $digits = $seconds.str_pad((string) $micros, 6, '0', STR_PAD_LEFT);
+ } elseif ($micros === 0) {
+ $sign = '-';
+ $digits = substr((string) $seconds, 1).'000000';
+ } else {
+ // seconds * 1e6 + micros is negative: its magnitude is (|seconds| - 1) * 1e6 + (1e6 - micros).
+ $sign = '-';
+ $digits = (-($seconds + 1)).str_pad((string) (1_000_000 - $micros), 6, '0', STR_PAD_LEFT);
+ }
+
+ return (float) ($sign.substr($digits, 0, -3).'.'.substr($digits, -3));
+ }
+}
diff --git a/packages/feature-flags/src/Evaluation/EvaluationError.php b/packages/feature-flags/src/Evaluation/EvaluationError.php
new file mode 100644
index 00000000..2606a485
--- /dev/null
+++ b/packages/feature-flags/src/Evaluation/EvaluationError.php
@@ -0,0 +1,26 @@
+ ErrorCode::FLAG_NOT_FOUND(),
+ self::ParseError => ErrorCode::PARSE_ERROR(),
+ self::TypeMismatch => ErrorCode::TYPE_MISMATCH(),
+ self::General => ErrorCode::GENERAL(),
+ };
+ }
+}
diff --git a/packages/feature-flags/src/Evaluation/EvaluationReason.php b/packages/feature-flags/src/Evaluation/EvaluationReason.php
new file mode 100644
index 00000000..6fe10d6b
--- /dev/null
+++ b/packages/feature-flags/src/Evaluation/EvaluationReason.php
@@ -0,0 +1,15 @@
+ self::Boolean,
+ is_int($default) => self::Integer,
+ is_float($default) => self::Float,
+ is_string($default) => self::String,
+ default => self::Object,
+ };
+ }
+
+ /** Float accepts int and float; Object accepts arrays and stdClass. */
+ public function accepts(mixed $value): bool
+ {
+ return match ($this) {
+ self::Boolean => is_bool($value),
+ self::String => is_string($value),
+ self::Integer => is_int($value),
+ self::Float => is_int($value) || is_float($value),
+ self::Object => is_array($value) || $value instanceof stdClass,
+ };
+ }
+
+ /**
+ * @return bool|string|int|float|array
+ */
+ public function zero(): bool|string|int|float|array
+ {
+ return match ($this) {
+ self::Boolean => false,
+ self::String => '',
+ self::Integer => 0,
+ self::Float => 0.0,
+ self::Object => [],
+ };
+ }
+}
diff --git a/packages/feature-flags/src/Evaluation/FlagdEvaluator.php b/packages/feature-flags/src/Evaluation/FlagdEvaluator.php
new file mode 100644
index 00000000..eeae0042
--- /dev/null
+++ b/packages/feature-flags/src/Evaluation/FlagdEvaluator.php
@@ -0,0 +1,26 @@
+ $attributes the evaluation-context attributes (targetingKey excluded)
+ */
+ public function evaluate(
+ FlagDocument $document,
+ string $flagKey,
+ FlagType $type,
+ mixed $default,
+ ?string $targetingKey = null,
+ array $attributes = [],
+ ): Resolution;
+}
diff --git a/packages/feature-flags/src/Evaluation/FlagdEvaluatorConfiguration.php b/packages/feature-flags/src/Evaluation/FlagdEvaluatorConfiguration.php
new file mode 100644
index 00000000..f60b531e
--- /dev/null
+++ b/packages/feature-flags/src/Evaluation/FlagdEvaluatorConfiguration.php
@@ -0,0 +1,28 @@
+ Fractional::evaluate(...),
+ 'sem_ver' => SemVer::evaluate(...),
+ 'starts_with' => StringOps::startsWith(...),
+ 'ends_with' => StringOps::endsWith(...),
+ ]);
+ }
+}
diff --git a/packages/feature-flags/src/Evaluation/Fractional.php b/packages/feature-flags/src/Evaluation/Fractional.php
new file mode 100644
index 00000000..c36ffa9f
--- /dev/null
+++ b/packages/feature-flags/src/Evaluation/Fractional.php
@@ -0,0 +1,173 @@
+> 32`, which stays an int on a 64-bit build because hash < 2^32 and
+ * totalWeight <= 2^31 - 1, so the product is below 2^63. A bucket is `[variant]` (weight 1) or
+ * `[variant, weight]`; the weight must be an integer (not a bool, not a float) and a negative one counts as 0.
+ * A malformed bucket, or weights summing past 2^31 - 1, make the operator null.
+ *
+ * Where the reference raises rather than answers, this throws a JsonLogicError (GENERAL): reading the bucket key
+ * from data that is not an object (`some`, `map`, … hand each item to the operator as its data) or from a
+ * `$flagd` that is not one, and joining a flag key and a targeting key that are not both text — except two
+ * numbers Python adds to zero, which it reads as no key (null). A PHP `[]` is an empty object here, as
+ * everywhere an object is expected. One limit is PHP's: a weight written as an integer beyond 64 bits decodes
+ * to a float and is refused, where Python clamps a huge negative one to 0.
+ */
+final class Fractional
+{
+ public const int MAX_WEIGHT_SUM = 2147483647;
+
+ /**
+ * @param list $args
+ *
+ * @throws JsonLogicError where the reference raises
+ */
+ public static function evaluate(mixed $data, array $args): mixed
+ {
+ if ($args === []) {
+ return null;
+ }
+
+ if (is_string($args[0])) {
+ $bucketBy = $args[0];
+ $args = array_slice($args, 1);
+ } else {
+ $bucketBy = self::contextBucketKey($data);
+ }
+
+ if ($bucketBy === null || $bucketBy === '') {
+ return null;
+ }
+
+ $fractions = [];
+ $total = 0;
+ foreach ($args as $arg) {
+ if (! Json::isList($arg) || $arg === [] || count($arg) > 2) {
+ return null;
+ }
+ $weight = 1;
+ if (count($arg) === 2) {
+ if (! is_int($arg[1])) {
+ return null;
+ }
+ $weight = max(0, $arg[1]);
+ }
+ // Python sums first and refuses the total after; both give null, and stopping here keeps $total an int.
+ if ($weight > self::MAX_WEIGHT_SUM - $total) {
+ return null;
+ }
+ $fractions[] = [$arg[0], $weight];
+ $total += $weight;
+ }
+
+ $bucket = (self::hash($bucketBy) * $total) >> 32;
+ $end = 0;
+ foreach ($fractions as [$variant, $weight]) {
+ $end += $weight;
+ if ($bucket < $end) {
+ return $variant;
+ }
+ }
+
+ return null;
+ }
+
+ public static function hash(string $bucketBy): int
+ {
+ /** @var array{1: int} $unpacked */
+ $unpacked = unpack('N', hash('murmur3a', $bucketBy, true));
+
+ return $unpacked[1];
+ }
+
+ /**
+ * The reference's `data.get("$flagd", {}).get("flagKey", "") + data.get("targetingKey")`, after its
+ * `if not targeting_key: return None`.
+ *
+ * @throws JsonLogicError
+ */
+ private static function contextBucketKey(mixed $data): ?string
+ {
+ if (! self::isObject($data)) {
+ throw new JsonLogicError(sprintf('fractional reads its bucket key from the data, which is %s, not an object.', get_debug_type($data)));
+ }
+
+ $context = Json::members($data);
+ $flagd = array_key_exists('$flagd', $context) ? $context['$flagd'] : [];
+ if (! self::isObject($flagd)) {
+ throw new JsonLogicError(sprintf('fractional reads $flagd.flagKey from an object, not %s.', get_debug_type($flagd)));
+ }
+
+ $flagdMembers = Json::members($flagd);
+ $seed = array_key_exists('flagKey', $flagdMembers) ? $flagdMembers['flagKey'] : '';
+ $targetingKey = $context['targetingKey'] ?? null;
+
+ if (! self::pythonTruthy($targetingKey)) {
+ return null;
+ }
+
+ if (is_string($seed) && is_string($targetingKey)) {
+ return $seed.$targetingKey;
+ }
+
+ // Python adds two numbers instead of joining them; a zero sum is falsy (null), any other fails in mmh3.
+ $left = self::pythonNumber($seed);
+ $right = self::pythonNumber($targetingKey);
+ if ($left !== null && $right !== null) {
+ $sum = $left + $right;
+ if ($sum === 0 || $sum === 0.0) {
+ return null;
+ }
+ }
+
+ throw new JsonLogicError(sprintf(
+ 'fractional cannot join a flag key (%s) and a targeting key (%s) into a bucket key.',
+ get_debug_type($seed),
+ get_debug_type($targetingKey),
+ ));
+ }
+
+ /** A stdClass, a non-list array, or `[]` (an empty object, as everywhere an object is expected). */
+ private static function isObject(mixed $value): bool
+ {
+ return $value instanceof stdClass || (is_array($value) && ($value === [] || ! array_is_list($value)));
+ }
+
+ /** Python's `bool()`: an empty container is false, NaN is true. */
+ private static function pythonTruthy(mixed $value): bool
+ {
+ return match (true) {
+ $value === null => false,
+ is_bool($value) => $value,
+ is_int($value) => $value !== 0,
+ is_float($value) => $value !== 0.0,
+ is_string($value) => $value !== '',
+ is_array($value) => $value !== [],
+ $value instanceof stdClass => get_object_vars($value) !== [],
+ default => true,
+ };
+ }
+
+ /** The number Python sees (a bool is an int there), or null for anything else. */
+ private static function pythonNumber(mixed $value): int|float|null
+ {
+ return match (true) {
+ is_bool($value) => (int) $value,
+ is_int($value), is_float($value) => $value,
+ default => null,
+ };
+ }
+}
diff --git a/packages/feature-flags/src/Evaluation/JsonLogic.php b/packages/feature-flags/src/Evaluation/JsonLogic.php
new file mode 100644
index 00000000..d12bd3e5
--- /dev/null
+++ b/packages/feature-flags/src/Evaluation/JsonLogic.php
@@ -0,0 +1,1092 @@
+', '<=', '>=', '!', '!!', '+', '*', '-', '/', '%',
+ 'in', 'min', 'max', 'cat', 'log', 'var', 'substr', 'merge', 'missing', 'missing_some',
+ ];
+
+ /** The whitespace float() strips: ASCII spaces, and every non-ASCII str.isspace() character. */
+ private const array FLOAT_SPACE = [
+ "\t", "\n", "\x0B", "\x0C", "\r", ' ', "\u{85}", "\u{A0}", "\u{1680}", "\u{2000}", "\u{2001}", "\u{2002}", "\u{2003}",
+ "\u{2004}", "\u{2005}", "\u{2006}", "\u{2007}", "\u{2008}", "\u{2009}", "\u{200A}", "\u{2028}", "\u{2029}", "\u{202F}",
+ "\u{205F}", "\u{3000}",
+ ];
+
+ /** Python's str.strip() whitespace (every str.isspace() character), which to_number() strips before its "inf" check. */
+ private const array STRIP_SPACE = [...self::FLOAT_SPACE, "\x1C", "\x1D", "\x1E", "\x1F"];
+
+ /**
+ * float()'s decimal grammar on lower-cased ASCII text without its underscores. Only single-character repeats:
+ * a repeated group would recurse once per repetition and fail on a long run of digits.
+ */
+ private const string FLOAT_SYNTAX = '/^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:e[+-]?[0-9]+)?$/D';
+
+ /** @var array the ASCII digit of each Unicode decimal digit met so far (a pure function of it) */
+ private static array $digits = [];
+
+ /**
+ * @param array): mixed> $operations flagd's operators, keyed by name; a
+ * name shared with a builtin replaces it
+ */
+ public function __construct(private readonly array $operations = []) {}
+
+ /**
+ * @throws UnknownOperator
+ * @throws JsonLogicError
+ */
+ public function apply(mixed $logic, mixed $data = null): mixed
+ {
+ return $this->evaluate($logic, $data, 0);
+ }
+
+ public static function truthy(mixed $value): bool
+ {
+ return match (true) {
+ $value === null => false,
+ is_bool($value) => $value,
+ is_float($value) => ! is_nan($value) && $value !== 0.0,
+ is_int($value) => $value !== 0,
+ is_string($value) => $value !== '',
+ Json::isList($value) => $value !== [],
+ default => true,
+ };
+ }
+
+ /**
+ * @throws JsonLogicError on data nested deeper than MAX_DATA_DEPTH
+ */
+ public static function toNumber(mixed $value): int|float
+ {
+ $number = self::numeric($value);
+
+ return is_bool($number) ? (int) $number : $number;
+ }
+
+ /**
+ * @throws JsonLogicError on data nested deeper than MAX_DATA_DEPTH
+ */
+ public static function toStr(mixed $value): string
+ {
+ return self::text($value, 0);
+ }
+
+ /**
+ * @throws JsonLogicError
+ */
+ private static function text(mixed $value, int $depth): string
+ {
+ if (is_string($value)) {
+ return $value;
+ }
+ if (is_bool($value)) {
+ return $value ? 'True' : 'False';
+ }
+ if (is_float($value)) {
+ return self::formatG15($value);
+ }
+ if (is_int($value)) {
+ return (string) $value;
+ }
+ if ($value === null) {
+ return 'null';
+ }
+ if (Json::isList($value)) {
+ self::nested($depth);
+ $parts = [];
+ foreach ($value as $item) {
+ $parts[] = self::text($item, $depth + 1);
+ }
+
+ return implode(',', $parts);
+ }
+
+ return '[object Object]';
+ }
+
+ /**
+ * @throws JsonLogicError on data nested deeper than MAX_DATA_DEPTH
+ */
+ public static function looseEquals(mixed $a, mixed $b): bool
+ {
+ $kindA = self::kind($a);
+ $kindB = self::kind($b);
+
+ if ($kindA === $kindB) {
+ // Two lists or two objects: the reference asks `a is b`. A stdClass has an identity to compare; an
+ // array is a value and has none, so two arrays are never the same one.
+ return $kindA === 'list' || $kindA === 'object' ? is_object($a) && $a === $b : $a === $b;
+ }
+
+ if (self::isNumeric($a) || self::isNumeric($b)) {
+ return self::compareNumbers(self::toNumber($a), self::toNumber($b)) === 0;
+ }
+
+ if ($a === null || $b === null) {
+ return false;
+ }
+
+ if (is_string($a)) {
+ return $a === self::toStr($b);
+ }
+
+ if (is_string($b)) {
+ return self::toStr($a) === $b;
+ }
+
+ return false;
+ }
+
+ /**
+ * Python's `==`. A container is compared with itself by identity first, as Python's own containers do, so an
+ * object (PHP's only containers with an identity) is equal to itself however it nests — even when it contains
+ * itself.
+ *
+ * @throws JsonLogicError on data nested deeper than MAX_DATA_DEPTH
+ */
+ public static function strictEquals(mixed $a, mixed $b): bool
+ {
+ return self::equal($a, $b, 0);
+ }
+
+ /**
+ * @throws JsonLogicError
+ */
+ private static function equal(mixed $a, mixed $b, int $depth): bool
+ {
+ if (is_object($a) && $a === $b) {
+ return true;
+ }
+
+ if (self::isNumeric($a) && self::isNumeric($b)) {
+ return self::compareNumbers(self::toNumber($a), self::toNumber($b)) === 0;
+ }
+
+ if (Json::isList($a) && Json::isList($b)) {
+ if (count($a) !== count($b)) {
+ return false;
+ }
+ self::nested($depth);
+ foreach ($a as $index => $item) {
+ if (! self::equal($item, $b[$index], $depth + 1)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ if (Json::isObject($a) && Json::isObject($b)) {
+ $left = Json::members($a);
+ $right = Json::members($b);
+ if (count($left) !== count($right)) {
+ return false;
+ }
+ self::nested($depth);
+ foreach ($left as $key => $item) {
+ if (! array_key_exists($key, $right) || ! self::equal($item, $right[$key], $depth + 1)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ return $a === $b;
+ }
+
+ /**
+ * @throws UnknownOperator
+ * @throws JsonLogicError
+ */
+ private function evaluate(mixed $logic, mixed $data, int $depth): mixed
+ {
+ if ($depth >= self::MAX_DEPTH) {
+ throw new JsonLogicError('JSON Logic nests deeper than '.self::MAX_DEPTH.' levels.');
+ }
+ $depth++;
+
+ if (Json::isList($logic)) {
+ $items = [];
+ foreach ($logic as $item) {
+ $items[] = $this->evaluate($item, $data, $depth);
+ }
+
+ return $items;
+ }
+
+ $operation = self::operation($logic);
+ if ($operation === null) {
+ return $logic;
+ }
+
+ [$op, $args] = $operation;
+
+ switch ($op) {
+ case 'if':
+ case '?:':
+ return $this->conditional($args, $data, $depth);
+ case 'and':
+ $current = null;
+ foreach ($args as $arg) {
+ $current = $this->evaluate($arg, $data, $depth);
+ if (! self::truthy($current)) {
+ return $current;
+ }
+ }
+
+ return $current;
+ case 'or':
+ $current = null;
+ foreach ($args as $arg) {
+ $current = $this->evaluate($arg, $data, $depth);
+ if (self::truthy($current)) {
+ return $current;
+ }
+ }
+
+ return $current;
+ case 'filter':
+ if (count($args) < 2) {
+ return [];
+ }
+ $items = $this->evaluate($args[0], $data, $depth);
+ if (! Json::isList($items)) {
+ return [];
+ }
+ $kept = [];
+ foreach ($items as $item) {
+ if (self::truthy($this->evaluate($args[1], $item, $depth))) {
+ $kept[] = $item;
+ }
+ }
+
+ return $kept;
+ case 'map':
+ if ($args === []) {
+ return [];
+ }
+ $items = $this->evaluate($args[0], $data, $depth);
+ if (! Json::isList($items)) {
+ return [];
+ }
+ $mapped = [];
+ foreach ($items as $item) {
+ $mapped[] = $this->evaluate($args[1] ?? null, $item, $depth);
+ }
+
+ return $mapped;
+ case 'reduce':
+ if ($args === []) {
+ return null;
+ }
+ $items = $this->evaluate($args[0], $data, $depth);
+ // The initial accumulator is the third argument as written: the reference does not evaluate it.
+ $accumulator = $args[2] ?? null;
+ if (! Json::isList($items)) {
+ return $accumulator;
+ }
+ foreach ($items as $item) {
+ $accumulator = $this->evaluate($args[1] ?? null, ['accumulator' => $accumulator, 'current' => $item], $depth);
+ }
+
+ return $accumulator;
+ case 'all':
+ if (count($args) < 2) {
+ return false;
+ }
+ $items = $this->evaluate($args[0], $data, $depth);
+ if (! Json::isList($items) || $items === []) {
+ return false;
+ }
+ foreach ($items as $item) {
+ if (! self::truthy($this->evaluate($args[1], $item, $depth))) {
+ return false;
+ }
+ }
+
+ return true;
+ case 'some':
+ case 'none':
+ if (count($args) < 2) {
+ return $op === 'none';
+ }
+ $items = $this->evaluate($args[0], $data, $depth);
+ if (! Json::isList($items)) {
+ return $op === 'none';
+ }
+ foreach ($items as $item) {
+ if (self::truthy($this->evaluate($args[1], $item, $depth))) {
+ return $op === 'some';
+ }
+ }
+
+ return $op === 'none';
+ }
+
+ $values = [];
+ foreach ($args as $arg) {
+ $values[] = $this->evaluate($arg, $data, $depth);
+ }
+
+ if (isset($this->operations[$op])) {
+ return $this->registered($op, $data, $values);
+ }
+
+ $a = $values[0] ?? null;
+ $b = $values[1] ?? null;
+ $c = $values[2] ?? null;
+
+ return match ($op) {
+ '==' => self::looseEquals($a, $b),
+ '!=' => ! self::looseEquals($a, $b),
+ '===' => self::strictEquals($a, $b),
+ '!==' => ! self::strictEquals($a, $b),
+ '<' => self::chain($a, $b, $c, -1, -1),
+ '<=' => self::chain($a, $b, $c, -1, 0),
+ '>' => self::chain($a, $b, $c, 1, 1),
+ '>=' => self::chain($a, $b, $c, 1, 0),
+ '!' => ! self::truthy($a),
+ '!!' => self::truthy($a),
+ '+' => self::sum($values),
+ '*' => self::product($values),
+ '-' => $b === null ? -self::toNumber($a) : self::toNumber($a) - self::toNumber($b),
+ '/' => self::divide($a, $b),
+ '%' => self::modulo($a, $b),
+ 'min' => self::extreme($values, -1),
+ 'max' => self::extreme($values, 1),
+ 'in' => self::in($a, $b),
+ 'cat' => self::concatenate($values),
+ 'substr' => self::substr($a, $b, $c),
+ 'merge' => self::merge($values),
+ 'var' => count($values) > 2
+ ? throw new JsonLogicError('var takes a key and a default, not '.count($values).' arguments.')
+ : self::variable($data, $a, $b),
+ 'missing' => self::missing($data, $values),
+ 'missing_some' => self::missingSome($data, $values[0] ?? 0, $b),
+ 'log' => $a,
+ default => $this->unrecognized($op),
+ };
+ }
+
+ /**
+ * @param list $values
+ *
+ * @throws JsonLogicError
+ */
+ private function registered(string $op, mixed $data, array $values): mixed
+ {
+ try {
+ return ($this->operations[$op])($data, $values);
+ } catch (JsonLogicError $failure) {
+ throw $failure;
+ } catch (Throwable $failure) {
+ throw new JsonLogicError("Operation [{$op}] failed: {$failure->getMessage()}", 0, $failure);
+ }
+ }
+
+ /**
+ * The reference walks a dotted name through its operation table: a first segment it does not hold is an
+ * unrecognized operation; one it holds is a function, and indexing a function is a TypeError (GENERAL).
+ *
+ * @throws UnknownOperator
+ * @throws JsonLogicError
+ */
+ private function unrecognized(string $op): never
+ {
+ $head = strstr($op, '.', true);
+ if ($head !== false && (isset($this->operations[$head]) || in_array($head, self::BUILTINS, true))) {
+ throw new JsonLogicError("Operation [{$head}] has no member operations, so [{$op}] cannot run.");
+ }
+
+ throw new UnknownOperator($op);
+ }
+
+ /**
+ * @param list $args
+ *
+ * @throws UnknownOperator
+ * @throws JsonLogicError
+ */
+ private function conditional(array $args, mixed $data, int $depth): mixed
+ {
+ $count = count($args);
+ $index = 0;
+
+ while ($index < $count - 1) {
+ if (self::truthy($this->evaluate($args[$index], $data, $depth))) {
+ return $this->evaluate($args[$index + 1], $data, $depth);
+ }
+ $index += 2;
+ }
+
+ return $index < $count ? $this->evaluate($args[$index], $data, $depth) : null;
+ }
+
+ /**
+ * A single-member object is an operation; everything else is data and evaluates to itself.
+ *
+ * @return array{0: string, 1: list}|null
+ */
+ private static function operation(mixed $logic): ?array
+ {
+ if (! Json::isObject($logic)) {
+ return null;
+ }
+
+ $members = Json::members($logic);
+ if (count($members) !== 1) {
+ return null;
+ }
+
+ $name = array_key_first($members);
+ $args = $members[$name];
+
+ return [(string) $name, Json::isList($args) ? $args : [$args]];
+ }
+
+ /**
+ * Refuses to go below a container at $depth when that would pass MAX_DATA_DEPTH.
+ *
+ * @throws JsonLogicError
+ */
+ private static function nested(int $depth): void
+ {
+ if ($depth >= self::MAX_DATA_DEPTH) {
+ throw new JsonLogicError('Data nests deeper than '.self::MAX_DATA_DEPTH.' levels (or contains itself).');
+ }
+ }
+
+ private static function kind(mixed $value): string
+ {
+ return match (true) {
+ $value === null => 'null',
+ is_bool($value) => 'bool',
+ is_int($value) => 'int',
+ is_float($value) => 'float',
+ is_string($value) => 'string',
+ Json::isList($value) => 'list',
+ default => 'object',
+ };
+ }
+
+ /**
+ * Python's numbers: int, float and bool (a bool IS an int there).
+ *
+ * @phpstan-assert-if-true bool|int|float $value
+ */
+ private static function isNumeric(mixed $value): bool
+ {
+ return is_int($value) || is_float($value) || is_bool($value);
+ }
+
+ /**
+ * The reference's to_number(), which hands a boolean back unchanged (it is already a Python number).
+ *
+ * @throws JsonLogicError on one-item lists nested deeper than MAX_DATA_DEPTH
+ */
+ private static function numeric(mixed $value, int $depth = 0): int|float|bool
+ {
+ if (self::isNumeric($value)) {
+ return $value;
+ }
+ if ($value === null) {
+ return 0;
+ }
+ if (Json::isList($value)) {
+ if (count($value) === 1) {
+ self::nested($depth);
+ }
+
+ return match (count($value)) {
+ 0 => 0,
+ 1 => self::numeric($value[0] ?? null, $depth + 1),
+ default => NAN,
+ };
+ }
+ if (is_string($value)) {
+ return self::parseFloat($value);
+ }
+
+ return NAN;
+ }
+
+ /**
+ * Python's ordering of two numbers, exact across int and float: -1, 0 or 1, or null when either is NaN.
+ */
+ private static function compareNumbers(int|float $a, int|float $b): ?int
+ {
+ if (is_int($a)) {
+ return is_int($b) ? $a <=> $b : self::compareIntWithFloat($a, $b);
+ }
+ if (is_int($b)) {
+ $order = self::compareIntWithFloat($b, $a);
+
+ return $order === null ? null : -$order;
+ }
+
+ return is_nan($a) || is_nan($b) ? null : $a <=> $b;
+ }
+
+ private static function compareIntWithFloat(int $int, float $float): ?int
+ {
+ if (is_nan($float)) {
+ return null;
+ }
+ if ($float >= 9223372036854775808.0) {
+ return -1;
+ }
+ if ($float < -9223372036854775808.0) {
+ return 1;
+ }
+
+ // Within the int range a float's floor is an exact int, so the comparison loses nothing.
+ $floor = floor($float);
+ $whole = (int) $floor;
+ if ($int !== $whole) {
+ return $int <=> $whole;
+ }
+
+ return $floor === $float ? 0 : -1;
+ }
+
+ /**
+ * `<`, `<=`, `>`, `>=` with an optional third argument (a between test); $strict is the order that must hold,
+ * $loose the other order also accepted (equal for `<=`/`>=`).
+ */
+ private static function chain(mixed $a, mixed $b, mixed $c, int $strict, int $loose): bool
+ {
+ $order = self::order($a, $b);
+ if ($order !== $strict && $order !== $loose) {
+ return false;
+ }
+ if ($c === null) {
+ return true;
+ }
+ $order = self::order($b, $c);
+
+ return $order === $strict || $order === $loose;
+ }
+
+ /** The reference's less_than() family: numbers if either side is one, else text if either side is text. */
+ private static function order(mixed $a, mixed $b): ?int
+ {
+ if (self::isNumeric($a) || self::isNumeric($b) || (! is_string($a) && ! is_string($b))) {
+ return self::compareNumbers(self::toNumber($a), self::toNumber($b));
+ }
+
+ return strcmp(self::toStr($a), self::toStr($b)) <=> 0;
+ }
+
+ /**
+ * @param list $values
+ */
+ private static function sum(array $values): int|float
+ {
+ $sum = 0;
+ foreach ($values as $value) {
+ $sum += self::toNumber($value);
+ }
+
+ return $sum;
+ }
+
+ /**
+ * @param list $values
+ */
+ private static function product(array $values): int|float
+ {
+ $product = 1;
+ foreach ($values as $value) {
+ $product *= self::toNumber($value);
+ }
+
+ return $product;
+ }
+
+ /**
+ * @throws JsonLogicError
+ */
+ private static function divide(mixed $a, mixed $b): float
+ {
+ $divisor = self::toNumber($b);
+ if ($divisor === 0 || $divisor === 0.0) {
+ throw new JsonLogicError('Division by zero.');
+ }
+
+ return (float) self::toNumber($a) / (float) $divisor;
+ }
+
+ /**
+ * @throws JsonLogicError
+ */
+ private static function modulo(mixed $a, mixed $b): int|float
+ {
+ $x = self::toNumber($a);
+ $y = self::toNumber($b);
+ if ($y === 0 || $y === 0.0) {
+ throw new JsonLogicError('Modulo by zero.');
+ }
+
+ if (is_int($x) && is_int($y)) {
+ $remainder = $x % $y;
+
+ return $remainder !== 0 && ($remainder < 0) !== ($y < 0) ? $remainder + $y : $remainder;
+ }
+
+ $remainder = fmod((float) $x, (float) $y);
+ if ($remainder === 0.0) {
+ return $y < 0 ? -0.0 : 0.0;
+ }
+
+ return ($remainder < 0) !== ($y < 0) ? $remainder + $y : $remainder;
+ }
+
+ /**
+ * Python's min()/max() over to_number(): the first of equals wins, NaN never replaces, a boolean stays one.
+ *
+ * @param list $values
+ */
+ private static function extreme(array $values, int $better): int|float|bool
+ {
+ if ($values === []) {
+ return NAN;
+ }
+
+ $best = self::numeric($values[0]);
+ foreach (array_slice($values, 1) as $value) {
+ $number = self::numeric($value);
+ if (self::compareNumbers(self::toNumber($number), self::toNumber($best)) === $better) {
+ $best = $number;
+ }
+ }
+
+ return $best;
+ }
+
+ private static function in(mixed $needle, mixed $haystack): bool
+ {
+ if (Json::isList($haystack)) {
+ foreach ($haystack as $item) {
+ if (self::strictEquals($needle, $item)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ return is_string($haystack) && str_contains($haystack, self::toStr($needle));
+ }
+
+ /**
+ * @param list $values
+ */
+ private static function concatenate(array $values): string
+ {
+ $text = '';
+ foreach ($values as $value) {
+ $text .= self::toStr($value);
+ }
+
+ return $text;
+ }
+
+ /**
+ * The reference's op_substr over code points: a negative index counts from the end, a negative length stops
+ * that far from the end, NaN reads as 0 (index) or as an empty slice (length).
+ *
+ * @throws JsonLogicError
+ */
+ private static function substr(mixed $string, mixed $index, mixed $length): string
+ {
+ $text = self::toStr($string);
+ $size = mb_strlen($text, 'UTF-8');
+ $start = self::toNumber($index);
+
+ if (is_float($start) && is_nan($start)) {
+ $from = 0;
+ } elseif ($start < 0) {
+ $back = -$start;
+ $from = $back >= $size ? 0 : $size - self::truncate($back);
+ } else {
+ $from = min(self::truncate($start), $size);
+ }
+
+ if ($length === null) {
+ $to = $size;
+ } else {
+ $count = self::toNumber($length);
+ if (is_float($count) && is_nan($count)) {
+ $to = $from;
+ } else {
+ $count = self::truncate($count);
+ if ($count < 0) {
+ $to = max($size + $count, $from);
+ } else {
+ $to = $count >= $size - $from ? $size : $from + $count;
+ }
+ }
+ }
+
+ return $to > $from ? mb_substr($text, $from, $to - $from, 'UTF-8') : '';
+ }
+
+ /**
+ * Python's int() of a number: toward zero, exact for any finite value (clamped here to the int range, which
+ * every caller then clamps to a string length), an OverflowError (GENERAL) for an infinity.
+ *
+ * @throws JsonLogicError
+ */
+ private static function truncate(int|float $number): int
+ {
+ if (is_int($number)) {
+ return $number;
+ }
+ if (is_infinite($number)) {
+ throw new JsonLogicError('Cannot convert an infinite number to an integer.');
+ }
+ if ($number >= 9223372036854775808.0) {
+ return PHP_INT_MAX;
+ }
+ if ($number < -9223372036854775808.0) {
+ return PHP_INT_MIN;
+ }
+
+ return (int) $number;
+ }
+
+ /**
+ * @param list $values
+ * @return list
+ */
+ private static function merge(array $values): array
+ {
+ $merged = [];
+ foreach ($values as $value) {
+ if (Json::isList($value)) {
+ array_push($merged, ...$value);
+ } else {
+ $merged[] = $value;
+ }
+ }
+
+ return $merged;
+ }
+
+ /**
+ * The reference's op_var: a number indexes a list or a string (an object holds only text keys, so a number
+ * never finds a member); text is a dot path whose list and string steps are canonical indexes or `length`.
+ *
+ * @throws JsonLogicError
+ */
+ private static function variable(mixed $data, mixed $key, mixed $default): mixed
+ {
+ if ($key === null || $key === '') {
+ return $data;
+ }
+
+ if (self::isNumeric($key)) {
+ if (! Json::isList($data) && ! is_string($data)) {
+ return $default;
+ }
+ $index = self::exactIndex($key);
+ if ($index === null || $index < 0 || $index >= self::length($data)) {
+ return $default;
+ }
+
+ return is_string($data) ? mb_substr($data, $index, 1, 'UTF-8') : $data[$index];
+ }
+
+ $value = $data;
+ foreach (explode('.', self::toStr($key)) as $property) {
+ if (Json::isList($value) || is_string($value)) {
+ if ($property === 'length') {
+ return self::length($value);
+ }
+ if (preg_match('/^(?:0|[1-9][0-9]*)$/D', $property) !== 1) {
+ return $default;
+ }
+ $position = (int) $property;
+ if ($position >= self::length($value)) {
+ return $default;
+ }
+ $value = is_string($value) ? mb_substr($value, $position, 1, 'UTF-8') : $value[$position];
+ } elseif (Json::isObject($value)) {
+ $members = Json::members($value);
+ $value = array_key_exists($property, $members) ? $members[$property] : null;
+ } else {
+ return $default;
+ }
+ }
+
+ return $value ?? $default;
+ }
+
+ /**
+ * A numeric `var` key as a list index: null when the reference's int(key) differs from the key (a fraction,
+ * NaN, a float beyond the int range), an OverflowError (GENERAL) for an infinity.
+ *
+ * @throws JsonLogicError
+ */
+ private static function exactIndex(int|float|bool $key): ?int
+ {
+ if (! is_float($key)) {
+ return (int) $key;
+ }
+ if (is_nan($key)) {
+ return null;
+ }
+ if (is_infinite($key)) {
+ throw new JsonLogicError('Cannot convert an infinite number to an integer.');
+ }
+ if ($key >= 9223372036854775808.0 || $key < -9223372036854775808.0) {
+ return null;
+ }
+
+ $index = (int) $key;
+
+ return (float) $index === $key ? $index : null;
+ }
+
+ /**
+ * @param list|string $value
+ */
+ private static function length(array|string $value): int
+ {
+ return is_string($value) ? mb_strlen($value, 'UTF-8') : count($value);
+ }
+
+ /**
+ * @param list $values
+ * @return list
+ *
+ * @throws JsonLogicError
+ */
+ private static function missing(mixed $data, array $values): array
+ {
+ $keys = $values !== [] && Json::isList($values[0]) ? $values[0] : $values;
+
+ $missing = [];
+ foreach ($keys as $key) {
+ $value = self::variable($data, $key, null);
+ if ($value === null || $value === '') {
+ $missing[] = $key;
+ }
+ }
+
+ return $missing;
+ }
+
+ /**
+ * @return list
+ *
+ * @throws JsonLogicError
+ */
+ private static function missingSome(mixed $data, mixed $need, mixed $keys): array
+ {
+ if (! Json::isList($keys)) {
+ return [];
+ }
+
+ $missing = self::missing($data, [$keys]);
+ $order = self::compareNumbers(count($keys) - count($missing), self::toNumber($need));
+
+ return $order !== null && $order >= 0 ? [] : $missing;
+ }
+
+ /**
+ * The reference's to_number() of text: NaN for "inf"/"+inf"/"-inf" once Python's strip() and lower() ran,
+ * else Python's float() — which reads Unicode decimal digits as digits and strips non-ASCII spaces — or NaN
+ * where float() raises.
+ */
+ private static function parseFloat(string $value): float
+ {
+ if (! mb_check_encoding($value, 'UTF-8')) {
+ return NAN;
+ }
+
+ if (in_array(strtolower(self::strip($value, self::STRIP_SPACE)), ['inf', '+inf', '-inf'], true)) {
+ return NAN;
+ }
+
+ $ascii = preg_replace_callback('/[^\x00-\x7F]/u', self::asciiDigit(...), self::strip($value, self::FLOAT_SPACE));
+ if ($ascii === null) {
+ return NAN;
+ }
+
+ $text = strtolower($ascii);
+ if (str_contains($text, '_')) {
+ // float() accepts an underscore only between two digits, then reads the text without it.
+ if (preg_match('/(? $spaces
+ */
+ private static function strip(string $value, array $spaces): string
+ {
+ $start = 0;
+ $end = strlen($value);
+ while ($start < $end) {
+ $lead = ord($value[$start]);
+ $width = $lead < 0x80 ? 1 : ($lead < 0xE0 ? 2 : ($lead < 0xF0 ? 3 : 4));
+ if (! in_array(substr($value, $start, $width), $spaces, true)) {
+ break;
+ }
+ $start += $width;
+ }
+ while ($end > $start) {
+ $from = $end - 1;
+ while ($from > $start && (ord($value[$from]) & 0xC0) === 0x80) {
+ $from--;
+ }
+ if (! in_array(substr($value, $from, $end - $from), $spaces, true)) {
+ break;
+ }
+ $end = $from;
+ }
+
+ return substr($value, $start, $end - $start);
+ }
+
+ /**
+ * float()'s view of one non-ASCII character: a Unicode decimal digit is its ASCII digit, anything else makes
+ * the text unparsable. Unicode encodes every decimal digit in runs of ten from 0, so a digit's value is its
+ * distance from the start of its runs modulo ten; no run starts right after a surrogate, so the walk back
+ * never asks mb_chr() for one.
+ *
+ * @param array $match
+ */
+ private static function asciiDigit(array $match): string
+ {
+ $character = $match[0] ?? '';
+ if (isset(self::$digits[$character])) {
+ return self::$digits[$character];
+ }
+ if (preg_match('/^\p{Nd}$/u', $character) !== 1) {
+ return '?';
+ }
+
+ $code = mb_ord($character, 'UTF-8');
+ $distance = 0;
+ while ($code - $distance > 0 && preg_match('/^\p{Nd}$/u', mb_chr($code - $distance - 1, 'UTF-8')) === 1) {
+ $distance++;
+ }
+
+ return self::$digits[$character] = (string) ($distance % 10);
+ }
+
+ /** Python's `'%.15g' % value`. */
+ private static function formatG15(float $value): string
+ {
+ if (is_nan($value)) {
+ return 'nan';
+ }
+ if (is_infinite($value)) {
+ return $value > 0 ? 'inf' : '-inf';
+ }
+ if ($value === 0.0) {
+ return fdiv(1.0, $value) < 0 ? '-0' : '0';
+ }
+
+ [$mantissa, $exponent] = explode('e', sprintf('%.14e', $value));
+ $power = (int) $exponent;
+
+ if ($power < -4 || $power >= 15) {
+ $digits = str_contains($mantissa, '.') ? rtrim(rtrim($mantissa, '0'), '.') : $mantissa;
+
+ return $digits.'e'.($power < 0 ? '-' : '+').str_pad((string) abs($power), 2, '0', STR_PAD_LEFT);
+ }
+
+ $fixed = sprintf('%.'.(14 - $power).'F', $value);
+
+ return str_contains($fixed, '.') ? rtrim(rtrim($fixed, '0'), '.') : $fixed;
+ }
+}
diff --git a/packages/feature-flags/src/Evaluation/JsonLogicError.php b/packages/feature-flags/src/Evaluation/JsonLogicError.php
new file mode 100644
index 00000000..7575d0c4
--- /dev/null
+++ b/packages/feature-flags/src/Evaluation/JsonLogicError.php
@@ -0,0 +1,10 @@
+ $open the ids of the objects being written on this path
+ */
+ private static function written(mixed $value, array $open, int $depth): ?string
+ {
+ if (is_string($value)) {
+ return self::quote($value);
+ }
+ if (is_bool($value)) {
+ return $value ? 'True' : 'False';
+ }
+ if ($value === null) {
+ return 'None';
+ }
+ if (is_int($value)) {
+ return (string) $value;
+ }
+ if (is_float($value)) {
+ return self::float($value);
+ }
+
+ if ((is_array($value) || $value instanceof stdClass) && $depth >= self::MAX_DEPTH) {
+ return null;
+ }
+
+ if (Json::isList($value)) {
+ $items = [];
+ foreach ($value as $item) {
+ $text = self::written($item, $open, $depth + 1);
+ if ($text === null) {
+ return null;
+ }
+ $items[] = $text;
+ }
+
+ return '['.implode(', ', $items).']';
+ }
+
+ if (is_array($value) || $value instanceof stdClass) {
+ if ($value instanceof stdClass) {
+ if (isset($open[spl_object_id($value)])) {
+ return '{...}';
+ }
+ $open[spl_object_id($value)] = true;
+ }
+ $items = [];
+ foreach (Json::members($value) as $name => $member) {
+ $key = self::quote((string) $name);
+ $text = self::written($member, $open, $depth + 1);
+ if ($key === null || $text === null) {
+ return null;
+ }
+ $items[] = $key.': '.$text;
+ }
+
+ return '{'.implode(', ', $items).'}';
+ }
+
+ return null;
+ }
+
+ /**
+ * Python's repr() of a float (what str() gives), independent of PHP's `precision` ini: the shortest digits
+ * that read back as the same float, fixed notation for 1e-4 <= |x| < 1e16 (with ".0" when whole), an
+ * exponent of at least two digits otherwise.
+ */
+ public static function float(float $value): string
+ {
+ if (is_nan($value)) {
+ return 'nan';
+ }
+ if (is_infinite($value)) {
+ return $value > 0 ? 'inf' : '-inf';
+ }
+
+ $sign = $value < 0 || ($value === 0.0 && fdiv(1.0, $value) < 0) ? '-' : '';
+ $magnitude = abs($value);
+
+ // Sixteen decimals in scientific notation is seventeen significant digits, which always read back.
+ $precision = 0;
+ while ($precision < 16 && (float) sprintf('%.'.$precision.'e', $magnitude) !== $magnitude) {
+ $precision++;
+ }
+ [$mantissa, $exponent] = explode('e', sprintf('%.'.$precision.'e', $magnitude));
+ $digits = rtrim(str_replace('.', '', $mantissa), '0');
+ $digits = $digits === '' ? '0' : $digits;
+ $point = (int) $exponent + 1;
+
+ if ($point <= -4 || $point > 16) {
+ $text = $digits[0].(strlen($digits) > 1 ? '.'.substr($digits, 1) : '');
+
+ return sprintf('%s%se%s%02d', $sign, $text, $point - 1 < 0 ? '-' : '+', abs($point - 1));
+ }
+
+ return $sign.match (true) {
+ $point <= 0 => '0.'.str_repeat('0', -$point).$digits,
+ $point >= strlen($digits) => $digits.str_repeat('0', $point - strlen($digits)).'.0',
+ default => substr($digits, 0, $point).'.'.substr($digits, $point),
+ };
+ }
+
+ /**
+ * repr() of text: single quotes unless the text holds a single quote and no double one; a backslash, the
+ * quote, \t, \n and \r escaped; any other control or unprintable character as \xNN, \uNNNN or \UNNNNNNNN.
+ */
+ private static function quote(string $text): ?string
+ {
+ if (! mb_check_encoding($text, 'UTF-8')) {
+ return null;
+ }
+
+ $quote = str_contains($text, "'") && ! str_contains($text, '"') ? '"' : "'";
+ $escaped = preg_replace_callback(
+ '/[\x00-\x1F\x7F\\\\\'"]|[^\x00-\x7F]/u',
+ static function (array $match) use ($quote): string {
+ $character = $match[0];
+
+ return match (true) {
+ $character === '\\' => '\\\\',
+ $character === $quote => '\\'.$quote,
+ $character === '"', $character === "'" => $character,
+ $character === "\t" => '\t',
+ $character === "\n" => '\n',
+ $character === "\r" => '\r',
+ strlen($character) === 1 => sprintf('\x%02x', ord($character)),
+ preg_match(self::NOT_PRINTABLE, $character) !== 1 => $character,
+ default => self::escape(mb_ord($character, 'UTF-8')),
+ };
+ },
+ $text,
+ );
+
+ return $escaped === null ? null : $quote.$escaped.$quote;
+ }
+
+ private static function escape(int|false $code): string
+ {
+ return match (true) {
+ $code === false => '',
+ $code <= 0xFF => sprintf('\x%02x', $code),
+ $code <= 0xFFFF => sprintf('\u%04x', $code),
+ default => sprintf('\U%08x', $code),
+ };
+ }
+}
diff --git a/packages/feature-flags/src/Evaluation/RefResolver.php b/packages/feature-flags/src/Evaluation/RefResolver.php
new file mode 100644
index 00000000..808dd807
--- /dev/null
+++ b/packages/feature-flags/src/Evaluation/RefResolver.php
@@ -0,0 +1,148 @@
+ $evaluators the document's `$evaluators`
+ */
+ public static function withinLimits(mixed $targeting, array $evaluators): bool
+ {
+ $budget = self::MAX_VALUES;
+
+ return self::fits($targeting, $evaluators, [], 1, $budget);
+ }
+
+ /**
+ * $targeting with every resolvable reference replaced by its expanded evaluator; a missing name or a cycle
+ * left as its `{"$ref": …}` object. Call it only within the limits (withinLimits()).
+ *
+ * @param array $evaluators the document's `$evaluators`
+ */
+ public static function expand(mixed $targeting, array $evaluators): mixed
+ {
+ return self::expanded($targeting, $evaluators, []);
+ }
+
+ /**
+ * @param array $evaluators
+ * @param array $resolving the evaluator names being resolved on this path
+ */
+ private static function fits(mixed $node, array $evaluators, array $resolving, int $level, int &$budget): bool
+ {
+ while (($name = self::resolvable($node, $evaluators, $resolving)) !== null) {
+ $resolving[$name] = true;
+ $node = $evaluators[$name];
+ if (--$budget < 0) {
+ return false;
+ }
+ }
+
+ if (--$budget < 0) {
+ return false;
+ }
+ if (! is_array($node) && ! $node instanceof stdClass) {
+ return true;
+ }
+ if ($level > self::MAX_DEPTH) {
+ return false;
+ }
+
+ foreach (is_array($node) ? $node : get_object_vars($node) as $child) {
+ if (! self::fits($child, $evaluators, $resolving, $level + 1, $budget)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * @param array $evaluators
+ * @param array $resolving
+ */
+ private static function expanded(mixed $node, array $evaluators, array $resolving): mixed
+ {
+ while (($name = self::resolvable($node, $evaluators, $resolving)) !== null) {
+ $resolving[$name] = true;
+ $node = $evaluators[$name];
+ }
+
+ if ($node instanceof stdClass) {
+ $object = new stdClass;
+ foreach (get_object_vars($node) as $member => $child) {
+ $object->{$member} = self::expanded($child, $evaluators, $resolving);
+ }
+
+ return $object;
+ }
+
+ if (is_array($node)) {
+ $array = [];
+ foreach ($node as $key => $child) {
+ $array[$key] = self::expanded($child, $evaluators, $resolving);
+ }
+
+ return $array;
+ }
+
+ return $node;
+ }
+
+ /**
+ * The evaluator name $node references, when that evaluator exists and is not already being resolved on this
+ * path; null otherwise (not a reference, a missing name, or a cycle).
+ *
+ * @param array $evaluators
+ * @param array $resolving
+ */
+ private static function resolvable(mixed $node, array $evaluators, array $resolving): ?string
+ {
+ if (! Json::isObject($node)) {
+ return null;
+ }
+
+ $members = Json::members($node);
+ $name = count($members) === 1 ? ($members['$ref'] ?? null) : null;
+
+ return is_string($name) && array_key_exists($name, $evaluators) && ! array_key_exists($name, $resolving) ? $name : null;
+ }
+}
diff --git a/packages/feature-flags/src/Evaluation/Resolution.php b/packages/feature-flags/src/Evaluation/Resolution.php
new file mode 100644
index 00000000..ad7c2b37
--- /dev/null
+++ b/packages/feature-flags/src/Evaluation/Resolution.php
@@ -0,0 +1,66 @@
+ $metadata
+ */
+ public function __construct(
+ public mixed $value,
+ public ?string $variant,
+ public EvaluationReason $reason,
+ public ?EvaluationError $error = null,
+ public ?string $errorMessage = null,
+ public array $metadata = [],
+ ) {}
+
+ /** The caller's default, no variant, reason ERROR and the code. */
+ public static function error(mixed $default, EvaluationError $error, string $message): self
+ {
+ return new self($default, null, EvaluationReason::Error, $error, $message);
+ }
+
+ public function withValue(mixed $value): self
+ {
+ return new self($value, $this->variant, $this->reason, $this->error, $this->errorMessage, $this->metadata);
+ }
+
+ /** The SDK's view: plain PHP arrays (never stdClass), the reason and error code as OpenFeature strings. */
+ public function toResolutionDetails(): ResolutionDetails
+ {
+ $value = Json::toPhp($this->value);
+ $builder = (new ResolutionDetailsBuilder)
+ ->withValue(is_bool($value) || is_string($value) || is_int($value) || is_float($value) || is_array($value) ? $value : null)
+ ->withReason($this->reason->value);
+
+ if ($this->variant !== null) {
+ $builder->withVariant($this->variant);
+ }
+
+ if ($this->error !== null) {
+ $builder->withError(new ResolutionError($this->error->toOpenFeature(), $this->errorMessage));
+ }
+
+ return $builder->build();
+ }
+}
diff --git a/packages/feature-flags/src/Evaluation/SemVer.php b/packages/feature-flags/src/Evaluation/SemVer.php
new file mode 100644
index 00000000..72103811
--- /dev/null
+++ b/packages/feature-flags/src/Evaluation/SemVer.php
@@ -0,0 +1,188 @@
+ $args
+ *
+ * @throws JsonLogicError where the reference raises
+ */
+ public static function evaluate(mixed $data, array $args): ?bool
+ {
+ if (count($args) !== 3) {
+ return null;
+ }
+
+ [$left, $operator, $right] = $args;
+ $a = self::parse($left);
+ $b = self::parse($right);
+ if ($a === null || $b === null) {
+ return null;
+ }
+
+ return match ($operator) {
+ '=' => self::compare($a, $b) === 0,
+ '!=' => self::compare($a, $b) !== 0,
+ '<' => self::compare($a, $b) < 0,
+ '<=' => self::compare($a, $b) <= 0,
+ '>' => self::compare($a, $b) > 0,
+ '>=' => self::compare($a, $b) >= 0,
+ '^' => $a['major'] === $b['major'],
+ '~' => $a['major'] === $b['major'] && $a['minor'] === $b['minor'],
+ default => null,
+ };
+ }
+
+ /**
+ * @return array{major: string, minor: string, patch: string, prerelease: string|null}|null
+ */
+ public static function parse(mixed $value): ?array
+ {
+ $version = match (true) {
+ is_string($value) => $value,
+ is_int($value) => (string) $value,
+ is_float($value) => PythonText::float($value),
+ default => null,
+ };
+ if ($version === null) {
+ return null;
+ }
+
+ if (str_starts_with($version, 'v') || str_starts_with($version, 'V')) {
+ $version = substr($version, 1);
+ }
+
+ $numeric = explode('+', explode('-', $version, 2)[0], 2)[0];
+ $dots = substr_count($numeric, '.');
+ if ($dots < 2) {
+ $version = $numeric.str_repeat('.0', 2 - $dots).substr($version, strlen($numeric));
+ }
+
+ if (preg_match(self::SHAPE, $version, $parts) !== 1) {
+ return null;
+ }
+
+ [, $major, $minor, $patch] = $parts;
+ $prerelease = ($parts[4] ?? '') !== '' ? $parts[4] : null;
+ $build = $parts[5] ?? null;
+
+ foreach ([$major, $minor, $patch] as $number) {
+ if (strlen($number) > self::PYTHON_INT_MAX_STR_DIGITS) {
+ return null;
+ }
+ }
+
+ if ($prerelease !== null) {
+ foreach (explode('.', $prerelease) as $identifier) {
+ // A numeric identifier has no leading zero; an alphanumeric one may (`00a`).
+ if ($identifier === '' || (ctype_digit($identifier) && $identifier !== '0' && $identifier[0] === '0')) {
+ return null;
+ }
+ }
+ }
+
+ if ($build !== null && in_array('', explode('.', $build), true)) {
+ return null;
+ }
+
+ return ['major' => $major, 'minor' => $minor, 'patch' => $patch, 'prerelease' => $prerelease];
+ }
+
+ /**
+ * python-semver's Version.compare(): the cores as integers, then the prereleases (a release above any of
+ * its prereleases).
+ *
+ * @param array{major: string, minor: string, patch: string, prerelease: string|null} $a
+ * @param array{major: string, minor: string, patch: string, prerelease: string|null} $b
+ *
+ * @throws JsonLogicError
+ */
+ private static function compare(array $a, array $b): int
+ {
+ foreach (['major', 'minor', 'patch'] as $part) {
+ $order = self::numeric($a[$part], $b[$part]);
+ if ($order !== 0) {
+ return $order;
+ }
+ }
+
+ $natural = self::natural($a['prerelease'], $b['prerelease']);
+
+ return match (true) {
+ $natural === 0 => 0,
+ $a['prerelease'] === null => 1,
+ $b['prerelease'] === null => -1,
+ default => $natural,
+ };
+ }
+
+ /**
+ * python-semver's _nat_cmp(): dot-separated identifiers, digits as numbers and below text, then the longer
+ * list above. Python converts every numeric identifier of both sides before it compares any.
+ *
+ * @throws JsonLogicError
+ */
+ private static function natural(?string $a, ?string $b): int
+ {
+ $left = explode('.', $a ?? '');
+ $right = explode('.', $b ?? '');
+
+ foreach ([...$left, ...$right] as $identifier) {
+ if (strlen($identifier) > self::PYTHON_INT_MAX_STR_DIGITS && ctype_digit($identifier)) {
+ throw new JsonLogicError('sem_ver: a numeric prerelease identifier exceeds the limit ('.self::PYTHON_INT_MAX_STR_DIGITS.' digits) for integer string conversion.');
+ }
+ }
+
+ $shared = min(count($left), count($right));
+ for ($i = 0; $i < $shared; $i++) {
+ $x = $left[$i];
+ $y = $right[$i];
+ $order = match (true) {
+ ctype_digit($x) && ctype_digit($y) => self::numeric($x, $y),
+ ctype_digit($x) => -1,
+ ctype_digit($y) => 1,
+ default => strcmp($x, $y) <=> 0,
+ };
+ if ($order !== 0) {
+ return $order;
+ }
+ }
+
+ return count($left) <=> count($right);
+ }
+
+ /** Non-negative integers of any length without leading zeros, compared exactly. */
+ private static function numeric(string $a, string $b): int
+ {
+ return strlen($a) <=> strlen($b) ?: strcmp($a, $b) <=> 0;
+ }
+}
diff --git a/packages/feature-flags/src/Evaluation/StringOps.php b/packages/feature-flags/src/Evaluation/StringOps.php
new file mode 100644
index 00000000..f2583a0d
--- /dev/null
+++ b/packages/feature-flags/src/Evaluation/StringOps.php
@@ -0,0 +1,29 @@
+ $args
+ */
+ public static function startsWith(mixed $data, array $args): ?bool
+ {
+ return count($args) === 2 && is_string($args[0]) && is_string($args[1]) ? str_starts_with($args[0], $args[1]) : null;
+ }
+
+ /**
+ * @param list $args
+ */
+ public static function endsWith(mixed $data, array $args): ?bool
+ {
+ return count($args) === 2 && is_string($args[0]) && is_string($args[1]) ? str_ends_with($args[0], $args[1]) : null;
+ }
+}
diff --git a/packages/feature-flags/src/Evaluation/UnknownOperator.php b/packages/feature-flags/src/Evaluation/UnknownOperator.php
new file mode 100644
index 00000000..edf9d26c
--- /dev/null
+++ b/packages/feature-flags/src/Evaluation/UnknownOperator.php
@@ -0,0 +1,14 @@
+|null $previous
+ * @param array|null $current
+ */
+ public function __construct(
+ public string $key,
+ public string $action,
+ public ?string $actor,
+ public ?array $previous,
+ public ?array $current,
+ ) {}
+}
diff --git a/packages/feature-flags/src/Event/FeatureFlagsChanged.php b/packages/feature-flags/src/Event/FeatureFlagsChanged.php
new file mode 100644
index 00000000..a490c213
--- /dev/null
+++ b/packages/feature-flags/src/Event/FeatureFlagsChanged.php
@@ -0,0 +1,30 @@
+ $changedKeys
+ */
+ public function __construct(
+ public array $changedKeys,
+ public string $origin,
+ ) {}
+}
diff --git a/packages/feature-flags/src/FeatureFlags.php b/packages/feature-flags/src/FeatureFlags.php
new file mode 100644
index 00000000..9e066864
--- /dev/null
+++ b/packages/feature-flags/src/FeatureFlags.php
@@ -0,0 +1,197 @@
+ true), on which the Firefly hooks record neither a metric nor an
+ * exposure event. A hook of your own can read it the same way (`$hints->get(FeatureFlags::PREVIEW_HINT) === true`).
+ * The SDK still merges the OpenFeature API's and the client's own evaluation contexts underneath (Firefly sets
+ * neither), as PyFly's preview does.
+ *
+ * The provider this application configured is (re)asserted on the OpenFeature API before evaluating: the global API
+ * is a process-wide singleton, and a second application booted in the same process (a test suite) must not
+ * evaluate through the first one's provider.
+ */
+final class FeatureFlags
+{
+ public const string CLIENT_NAME = 'firefly';
+
+ /** The hook hint of a preview evaluation: Firefly's hooks record no metric and no exposure event for it. */
+ public const string PREVIEW_HINT = 'firefly.preview';
+
+ private ?Client $client = null;
+
+ /**
+ * @param list $hooks attached to the `firefly` client only, so a reboot never accumulates them
+ */
+ public function __construct(
+ private readonly Provider $provider,
+ private readonly EvaluationContextResolver $context,
+ private readonly array $hooks = [],
+ private readonly ?FlagDocumentSource $documents = null,
+ private readonly ?API $api = null,
+ ) {}
+
+ public function client(): Client
+ {
+ $api = $this->api ?? OpenFeatureAPI::getInstance();
+ if ($api->getProvider() !== $this->provider) {
+ $api->setProvider($this->provider);
+ }
+
+ if ($this->client === null) {
+ $client = $api->getClient(self::CLIENT_NAME, Version::VERSION);
+ $client->setHooks([...$client->getHooks(), ...$this->hooks]);
+ $this->client = $client;
+ }
+
+ return $this->client;
+ }
+
+ public function provider(): Provider
+ {
+ return $this->provider;
+ }
+
+ /**
+ * @param array $context
+ */
+ public function isEnabled(string $key, bool $default = false, array $context = [], ?string $targetingKey = null): bool
+ {
+ return $this->client()->getBooleanValue($key, $default, $this->context->resolve($context, $targetingKey)) ?? $default;
+ }
+
+ /**
+ * @param array $context
+ */
+ public function getString(string $key, string $default, array $context = [], ?string $targetingKey = null): string
+ {
+ return $this->client()->getStringValue($key, $default, $this->context->resolve($context, $targetingKey)) ?? $default;
+ }
+
+ /**
+ * @param array $context
+ */
+ public function getInt(string $key, int $default, array $context = [], ?string $targetingKey = null): int
+ {
+ return $this->client()->getIntegerValue($key, $default, $this->context->resolve($context, $targetingKey));
+ }
+
+ /**
+ * @param array $context
+ */
+ public function getFloat(string $key, float $default, array $context = [], ?string $targetingKey = null): float
+ {
+ return $this->client()->getFloatValue($key, $default, $this->context->resolve($context, $targetingKey));
+ }
+
+ /**
+ * @param array $default
+ * @param array $context
+ * @return array
+ */
+ public function getObject(string $key, array $default = [], array $context = [], ?string $targetingKey = null): array
+ {
+ return $this->client()->getObjectValue($key, $default, $this->context->resolve($context, $targetingKey));
+ }
+
+ /**
+ * The whole evaluation; the type is the default's type. `ambient: false` is a preview (see the class).
+ *
+ * @param bool|string|int|float|array $default
+ * @param array $context
+ */
+ public function details(string $key, bool|string|int|float|array $default, array $context = [], ?string $targetingKey = null, bool $ambient = true): FlagEvaluation
+ {
+ $evaluationContext = $this->context->resolve($context, $targetingKey, $ambient);
+ $options = $ambient ? null : new EvaluationOptions([], new HookHints([self::PREVIEW_HINT => true]));
+ $client = $this->client();
+
+ $details = match (true) {
+ is_bool($default) => $client->getBooleanDetails($key, $default, $evaluationContext, $options),
+ is_string($default) => $client->getStringDetails($key, $default, $evaluationContext, $options),
+ is_int($default) => $client->getIntegerDetails($key, $default, $evaluationContext, $options),
+ is_float($default) => $client->getFloatDetails($key, $default, $evaluationContext, $options),
+ default => $client->getObjectDetails($key, $default, $evaluationContext, $options),
+ };
+
+ return FlagEvaluation::fromDetails($key, $details, $this->metadata($key));
+ }
+
+ /**
+ * The variant the flag resolves to, evaluated with the flag's own type when the flag is Firefly's (string
+ * evaluation otherwise); null when the flag is missing, disabled, has no matching variant or fails.
+ *
+ * @param array $context
+ */
+ public function variant(string $key, array $context = [], ?string $targetingKey = null): ?string
+ {
+ $type = $this->flagType($key) ?? FlagType::String;
+ $evaluation = $this->details($key, $type->zero(), $context, $targetingKey);
+
+ return $evaluation->errorCode === null ? $evaluation->variant : null;
+ }
+
+ private function flagType(string $key): ?FlagType
+ {
+ try {
+ return $this->documents?->document()->flag($key)?->flagType();
+ } catch (Throwable) {
+ return null;
+ }
+ }
+
+ /**
+ * The flag's metadata over the document's (scalars only), or none for a flag Firefly does not hold.
+ *
+ * @return array
+ */
+ private function metadata(string $key): array
+ {
+ try {
+ $document = $this->documents?->document();
+ } catch (Throwable) {
+ return [];
+ }
+
+ $flag = $document?->flag($key);
+ if ($document === null || $flag === null) {
+ return [];
+ }
+
+ $shared = array_filter($document->metadata, static fn (mixed $value): bool => is_bool($value) || is_int($value) || is_float($value) || is_string($value));
+
+ return array_replace($shared, $flag->metadata());
+ }
+}
diff --git a/packages/feature-flags/src/FeatureFlagsAutoConfiguration.php b/packages/feature-flags/src/FeatureFlagsAutoConfiguration.php
new file mode 100644
index 00000000..bfaed9fa
--- /dev/null
+++ b/packages/feature-flags/src/FeatureFlagsAutoConfiguration.php
@@ -0,0 +1,127 @@
+getAll(FlagSource::class), static fn (object $source): bool => $source instanceof FlagSource));
+
+ /** @var list $sources */
+ return new FlagRegistry($sources, new CacheBook($cache, $logger), $events, $logger);
+ }
+
+ #[Bean]
+ #[ConditionalOnProperty(name: 'firefly.feature-flags.enabled', havingValue: 'true')]
+ #[ConditionalOnMissingBean(Provider::class)]
+ public function openFeatureProvider(FlagRegistry $registry, FlagdEvaluator $evaluator, ?LoggerInterface $logger = null): Provider
+ {
+ $provider = new FireflyFlagProvider($registry, $evaluator);
+ if ($logger !== null) {
+ $provider->setLogger($logger);
+ }
+
+ return $provider;
+ }
+
+ #[Bean]
+ #[ConditionalOnProperty(name: 'firefly.feature-flags.enabled', havingValue: 'true')]
+ #[ConditionalOnMissingBean(EvaluationContextResolver::class)]
+ public function evaluationContextResolver(FireflyContainer $beans, ?LoggerInterface $logger = null): EvaluationContextResolver
+ {
+ $contributors = array_values(array_filter($beans->getAll(EvaluationContextContributor::class), static fn (object $contributor): bool => $contributor instanceof EvaluationContextContributor));
+
+ /** @var list $contributors */
+ return new EvaluationContextResolver($contributors, $logger ?? new NullLogger);
+ }
+
+ #[Bean]
+ #[ConditionalOnProperty(name: 'firefly.feature-flags.enabled', havingValue: 'true')]
+ #[ConditionalOnMissingBean(FeatureFlags::class)]
+ public function featureFlags(
+ Provider $provider,
+ EvaluationContextResolver $context,
+ FeatureFlagMetrics $metrics,
+ ApplicationEventPublisher $events,
+ FeatureFlagsSettings $settings,
+ FireflyContainer $beans,
+ ?LoggerInterface $logger = null,
+ ): FeatureFlags {
+ $logger ??= new NullLogger;
+ $hooks = [new MetricsHook($metrics, $logger)];
+ if ($settings->publishEvaluations) {
+ $hooks[] = new ExposureEventHook($events, $logger);
+ }
+
+ $registry = $provider instanceof FireflyFlagProvider && $beans->has(FlagRegistry::class)
+ ? $beans->get(FlagRegistry::class)
+ : null;
+
+ return new FeatureFlags($provider, $context, $hooks, $registry instanceof FlagDocumentSource ? $registry : null);
+ }
+
+ #[Bean]
+ #[ConditionalOnProperty(name: 'firefly.feature-flags.enabled', havingValue: 'true')]
+ #[ConditionalOnMissingBean(Client::class)]
+ public function featureFlagsClient(FeatureFlags $flags): Client
+ {
+ return $flags->client();
+ }
+}
diff --git a/packages/feature-flags/src/FeatureFlagsServiceProvider.php b/packages/feature-flags/src/FeatureFlagsServiceProvider.php
new file mode 100644
index 00000000..f3d86e45
--- /dev/null
+++ b/packages/feature-flags/src/FeatureFlagsServiceProvider.php
@@ -0,0 +1,25 @@
+ */
+ public const array DISABLED_STATUSES = [404, 403, 503];
+
+ /** @var list */
+ public const array STORE_DRIVERS = ['database', 'memory'];
+
+ /**
+ * @param array $flags
+ * @param array $evaluators
+ */
+ public function __construct(
+ public bool $enabled,
+ public array $flags,
+ public array $evaluators,
+ public FileSourceSettings $file,
+ public HttpSourceSettings $http,
+ public StoreSourceSettings $store,
+ public string $tenantAttribute,
+ public int $disabledStatus,
+ public bool $publishEvaluations,
+ public bool $writes,
+ public ServerSettings $server,
+ ) {}
+
+ public static function fromConfig(Config $config): self
+ {
+ $durations = new DurationReader($config);
+
+ $file = new FileSourceSettings(
+ enabled: $config->bool('firefly.feature-flags.sources.file.enabled', false),
+ path: $config->string('firefly.feature-flags.sources.file.path', ''),
+ refreshInterval: $durations->get('firefly.feature-flags.sources.file.refresh-interval', 5.0),
+ );
+ if ($file->enabled && trim($file->path) === '') {
+ throw new ConfigurationException('firefly.feature-flags.sources.file.enabled is on but firefly.feature-flags.sources.file.path is empty: name the flagd document (.json, .yaml or .yml) to watch.');
+ }
+
+ $http = new HttpSourceSettings(
+ enabled: $config->bool('firefly.feature-flags.sources.http.enabled', false),
+ url: $config->string('firefly.feature-flags.sources.http.url', ''),
+ token: $config->string('firefly.feature-flags.sources.http.token', ''),
+ refreshInterval: $durations->get('firefly.feature-flags.sources.http.refresh-interval', 30.0, refuseZeroBecause: 'zero would send the remote server one conditional GET per request, with no stampede lock'),
+ timeout: $durations->get('firefly.feature-flags.sources.http.timeout', 2.0, refuseZeroBecause: "Laravel's HTTP client reads zero as no timeout at all"),
+ );
+ if ($http->enabled && trim($http->url) === '') {
+ throw new ConfigurationException('firefly.feature-flags.sources.http.enabled is on but firefly.feature-flags.sources.http.url is empty: name the sync endpoint to poll.');
+ }
+
+ $driver = $config->string('firefly.feature-flags.sources.store.driver', 'database');
+ if (! in_array($driver, self::STORE_DRIVERS, true)) {
+ throw new ConfigurationException("firefly.feature-flags.sources.store.driver must be 'database' or 'memory', got [{$driver}].");
+ }
+ $connection = $config->get('firefly.feature-flags.sources.store.connection');
+ if ($connection !== null && ! is_string($connection)) {
+ throw new ConfigurationException('firefly.feature-flags.sources.store.connection must name a database connection (null or an empty string for the default one), got '.get_debug_type($connection).'.');
+ }
+ $store = new StoreSourceSettings(
+ enabled: $config->bool('firefly.feature-flags.sources.store.enabled', false),
+ driver: $driver,
+ connection: $connection === null || $connection === '' ? null : $connection,
+ refreshInterval: $durations->get('firefly.feature-flags.sources.store.refresh-interval', 5.0),
+ );
+
+ $server = new ServerSettings(
+ enabled: $config->bool('firefly.feature-flags.server.enabled', false),
+ path: '/'.ltrim($config->string('firefly.feature-flags.server.path', '/feature-flags/flagd.json'), '/'),
+ token: $config->string('firefly.feature-flags.server.token', ''),
+ allowAnonymous: $config->bool('firefly.feature-flags.server.allow-anonymous', false),
+ );
+ if ($server->enabled && $server->token === '' && ! $server->allowAnonymous) {
+ throw new ConfigurationException('firefly.feature-flags.server.enabled is on without firefly.feature-flags.server.token: set a token (clients send it as a bearer token) or set firefly.feature-flags.server.allow-anonymous to true.');
+ }
+ if ($server->enabled && $server->path === '/') {
+ throw new ConfigurationException('firefly.feature-flags.server.enabled is on but firefly.feature-flags.server.path is the application root: name a path such as /feature-flags/flagd.json.');
+ }
+
+ return new self(
+ enabled: $config->bool('firefly.feature-flags.enabled', false),
+ flags: $config->array('firefly.feature-flags.flags', []),
+ evaluators: $config->array('firefly.feature-flags.evaluators', []),
+ file: $file,
+ http: $http,
+ store: $store,
+ tenantAttribute: $config->string('firefly.feature-flags.context.tenant-attribute', 'tenant'),
+ disabledStatus: self::disabledStatus($config),
+ publishEvaluations: $config->bool('firefly.feature-flags.events.evaluations', false),
+ writes: $config->bool('firefly.feature-flags.management.writes', false),
+ server: $server,
+ );
+ }
+
+ /** web.disabled-status, readable while the subsystem is off (the gate needs it). */
+ public static function disabledStatus(Config $config): int
+ {
+ $status = $config->int('firefly.feature-flags.web.disabled-status', 404);
+ if (! in_array($status, self::DISABLED_STATUSES, true)) {
+ throw new ConfigurationException("firefly.feature-flags.web.disabled-status must be 404, 403 or 503, got [{$status}].");
+ }
+
+ return $status;
+ }
+}
diff --git a/packages/feature-flags/src/FeatureFlagsWiringProvider.php b/packages/feature-flags/src/FeatureFlagsWiringProvider.php
new file mode 100644
index 00000000..c6b1c44a
--- /dev/null
+++ b/packages/feature-flags/src/FeatureFlagsWiringProvider.php
@@ -0,0 +1,52 @@
+
+ */
+ public function passes(): array
+ {
+ return [new FeatureFlagRouteGatingPass, new FlagdSyncRouteRegistrar];
+ }
+
+ public function boot(): void
+ {
+ if ($this->app->runningInConsole()) {
+ $this->commands([FlagsCommand::class]);
+ }
+
+ $migrations = dirname(__DIR__).'/database/migrations';
+ $this->publishes([$migrations.'/'.self::MIGRATION => $this->app->databasePath('migrations/'.self::MIGRATION)], 'firefly-feature-flags-migrations');
+
+ $config = new Config($this->app->make('config'));
+ if ($config->bool('firefly.feature-flags.enabled', false)
+ && $config->bool('firefly.feature-flags.sources.store.enabled', false)
+ && $config->string('firefly.feature-flags.sources.store.driver', 'database') === 'database') {
+ $this->loadMigrationsFrom($migrations);
+ }
+
+ $this->callAfterResolving('blade.compiler', static function (BladeCompiler $blade): void {
+ BladeDirectives::register($blade);
+ });
+ }
+}
diff --git a/packages/feature-flags/src/FlagEvaluation.php b/packages/feature-flags/src/FlagEvaluation.php
new file mode 100644
index 00000000..e03fa487
--- /dev/null
+++ b/packages/feature-flags/src/FlagEvaluation.php
@@ -0,0 +1,89 @@
+ $metadata
+ */
+ public function __construct(
+ public string $key,
+ public mixed $value,
+ public ?string $variant,
+ public string $reason,
+ public ?string $errorCode = null,
+ public ?string $errorMessage = null,
+ public array $metadata = [],
+ ) {}
+
+ /**
+ * An evaluation made through an OpenFeature client; $metadata applies only when it did not fail.
+ *
+ * @param array $metadata
+ */
+ public static function fromDetails(string $key, EvaluationDetails $details, array $metadata): self
+ {
+ $error = $details->getError();
+
+ return new self(
+ $key,
+ ValueCopy::of($details->getValue()),
+ $error === null ? $details->getVariant() : null,
+ $error === null ? ($details->getReason() ?? 'UNKNOWN') : 'ERROR',
+ $error?->getResolutionErrorCode()->getValue(),
+ $error?->getResolutionErrorMessage(),
+ $error === null ? $metadata : [],
+ );
+ }
+
+ /** An evaluation made by FireflyFlagProvider::resolution() (or an evaluator), metadata included. */
+ public static function fromResolution(string $key, Resolution $resolution): self
+ {
+ return new self(
+ $key,
+ ValueCopy::of($resolution->value),
+ $resolution->variant,
+ $resolution->reason->value,
+ $resolution->error?->value,
+ $resolution->errorMessage,
+ $resolution->metadata,
+ );
+ }
+
+ /**
+ * The contract's `evaluate` body (CONTRACT.md "The `flags` actuator endpoint"); the metadata is always a JSON
+ * object, `{}` when there is none.
+ *
+ * @return array{key: string, value: mixed, variant: ?string, reason: string, errorCode: ?string, metadata: stdClass|array}
+ */
+ public function toArray(): array
+ {
+ /** @var stdClass|array $metadata */
+ $metadata = Json::object($this->metadata);
+
+ return [
+ 'key' => $this->key,
+ 'value' => $this->value,
+ 'variant' => $this->variant,
+ 'reason' => $this->reason,
+ 'errorCode' => $this->errorCode,
+ 'metadata' => $metadata,
+ ];
+ }
+}
diff --git a/packages/feature-flags/src/Gating/BladeDirectives.php b/packages/feature-flags/src/Gating/BladeDirectives.php
new file mode 100644
index 00000000..3b3e2554
--- /dev/null
+++ b/packages/feature-flags/src/Gating/BladeDirectives.php
@@ -0,0 +1,24 @@
+if('featureflag', static fn (string $key, bool $default = false): bool => self::gate()->allows($key, null, $default));
+ $blade->if('featurevariant', static fn (string $key, string $variant): bool => self::gate()->allows($key, $variant));
+ }
+
+ private static function gate(): FeatureFlagGate
+ {
+ /** @var FeatureFlagGate */
+ return Container::getInstance()->make(FeatureFlagGate::class);
+ }
+}
diff --git a/packages/feature-flags/src/Gating/FeatureFlag.php b/packages/feature-flags/src/Gating/FeatureFlag.php
new file mode 100644
index 00000000..ef09bda8
--- /dev/null
+++ b/packages/feature-flags/src/Gating/FeatureFlag.php
@@ -0,0 +1,19 @@
+scanProxyAdvice($psr4);
+ }
+
+ public function render(array $row): string
+ {
+ return '\\'.FeatureFlagMethodDescriptor::class.'::fromArray('.var_export($row, true).')';
+ }
+}
diff --git a/packages/feature-flags/src/Gating/FeatureFlagDisabledException.php b/packages/feature-flags/src/Gating/FeatureFlagDisabledException.php
new file mode 100644
index 00000000..c3ac09df
--- /dev/null
+++ b/packages/feature-flags/src/Gating/FeatureFlagDisabledException.php
@@ -0,0 +1,29 @@
+ ['ACCESS_DENIED', 'Access denied', ErrorCategory::Security, 403],
+ 503 => ['SERVICE_UNAVAILABLE', 'Service unavailable', ErrorCategory::Infrastructure, 503],
+ default => ['RESOURCE_NOT_FOUND', 'Resource not found', ErrorCategory::Business, 404],
+ };
+
+ parent::__construct($message, $code, $httpStatus, $category, ErrorSeverity::Info);
+ }
+
+ public function flagKey(): string
+ {
+ return $this->flagKey;
+ }
+}
diff --git a/packages/feature-flags/src/Gating/FeatureFlagGate.php b/packages/feature-flags/src/Gating/FeatureFlagGate.php
new file mode 100644
index 00000000..aa4f5779
--- /dev/null
+++ b/packages/feature-flags/src/Gating/FeatureFlagGate.php
@@ -0,0 +1,49 @@
+disabledStatus = FeatureFlagsSettings::disabledStatus($config);
+ }
+
+ public function allows(string $key, ?string $variant = null, bool $default = false): bool
+ {
+ try {
+ $flags = $this->beans->has(FeatureFlags::class) ? $this->beans->get(FeatureFlags::class) : null;
+ if (! $flags instanceof FeatureFlags) {
+ return $default;
+ }
+
+ if ($variant === null) {
+ return $flags->isEnabled($key, $default);
+ }
+
+ $resolved = $flags->variant($key);
+
+ return $resolved === null ? $default : $resolved === $variant;
+ } catch (Throwable) {
+ return $default;
+ }
+ }
+
+ public function disabled(string $key): FeatureFlagDisabledException
+ {
+ return new FeatureFlagDisabledException($key, $this->disabledStatus);
+ }
+}
diff --git a/packages/feature-flags/src/Gating/FeatureFlagMethodDescriptor.php b/packages/feature-flags/src/Gating/FeatureFlagMethodDescriptor.php
new file mode 100644
index 00000000..d72999fa
--- /dev/null
+++ b/packages/feature-flags/src/Gating/FeatureFlagMethodDescriptor.php
@@ -0,0 +1,49 @@
+class.'::'.$this->method;
+ }
+
+ public function middleware(): string
+ {
+ return FeatureFlagGate::MIDDLEWARE_ALIAS.':'.$this->key.','.($this->variant ?? '').','.($this->default ? 'true' : 'false');
+ }
+
+ /** @return FeatureFlagMethodRow */
+ public function toArray(): array
+ {
+ return [
+ 'class' => $this->class,
+ 'method' => $this->method,
+ 'key' => $this->key,
+ 'variant' => $this->variant,
+ 'default' => $this->default,
+ 'fallback' => $this->fallback,
+ 'route' => $this->route,
+ ];
+ }
+
+ /** @param FeatureFlagMethodRow $data */
+ public static function fromArray(array $data): self
+ {
+ return new self($data['class'], $data['method'], $data['key'], $data['variant'] ?? null, $data['default'] ?? false, $data['fallback'] ?? null, $data['route'] ?? false);
+ }
+}
diff --git a/packages/feature-flags/src/Gating/FeatureFlagMethodInterceptor.php b/packages/feature-flags/src/Gating/FeatureFlagMethodInterceptor.php
new file mode 100644
index 00000000..09df7491
--- /dev/null
+++ b/packages/feature-flags/src/Gating/FeatureFlagMethodInterceptor.php
@@ -0,0 +1,71 @@
+> */
+ private WeakMap $fallbacks;
+
+ public function __construct(
+ private readonly FeatureFlagGate $gate,
+ private readonly RouteGateDecisions $decisions,
+ ) {
+ $this->fallbacks = new WeakMap;
+ }
+
+ public function invoke(MethodInvocation $invocation): mixed
+ {
+ $rule = $invocation->descriptor(FeatureFlagMethodDescriptor::class);
+ if ($rule === null) {
+ return $invocation->proceed();
+ }
+
+ $receiver = $invocation->getThis();
+ $method = strtolower($invocation->getMethod());
+ if (($this->fallbacks[$receiver][$method] ?? 0) > 0) {
+ return $invocation->proceed();
+ }
+
+ if ($rule->route && $rule->fallback === null && $this->decisions->passed($rule)) {
+ return $invocation->proceed();
+ }
+
+ if ($this->gate->allows($rule->key, $rule->variant, $rule->default)) {
+ return $invocation->proceed();
+ }
+
+ if ($rule->fallback === null) {
+ throw $this->gate->disabled($rule->key);
+ }
+
+ $fallbackMethod = strtolower($rule->fallback);
+ $active = $this->fallbacks[$receiver] ?? [];
+ $active[$fallbackMethod] = ($active[$fallbackMethod] ?? 0) + 1;
+ $this->fallbacks[$receiver] = $active;
+
+ try {
+ /** @var callable $fallback */
+ $fallback = [$receiver, $rule->fallback];
+
+ return $fallback(...$invocation->getArguments());
+ } finally {
+ $active = $this->fallbacks[$receiver];
+ if (--$active[$fallbackMethod] === 0) {
+ unset($active[$fallbackMethod]);
+ }
+ if ($active === []) {
+ unset($this->fallbacks[$receiver]);
+ } else {
+ $this->fallbacks[$receiver] = $active;
+ }
+ }
+ }
+}
diff --git a/packages/feature-flags/src/Gating/FeatureFlagMiddleware.php b/packages/feature-flags/src/Gating/FeatureFlagMiddleware.php
new file mode 100644
index 00000000..bd8df191
--- /dev/null
+++ b/packages/feature-flags/src/Gating/FeatureFlagMiddleware.php
@@ -0,0 +1,31 @@
+gate->allows($key, $variantName, $defaultEnabled)) {
+ throw $this->gate->disabled($key);
+ }
+
+ $this->decisions->record($request, $key, $variantName, $defaultEnabled);
+
+ return $next($request);
+ }
+}
diff --git a/packages/feature-flags/src/Gating/FeatureFlagRouteGatingPass.php b/packages/feature-flags/src/Gating/FeatureFlagRouteGatingPass.php
new file mode 100644
index 00000000..63ff8cdf
--- /dev/null
+++ b/packages/feature-flags/src/Gating/FeatureFlagRouteGatingPass.php
@@ -0,0 +1,84 @@
+container;
+ /** @var Router $router */
+ $router = $container->make('router');
+ $router->aliasMiddleware(FeatureFlagGate::MIDDLEWARE_ALIAS, FeatureFlagMiddleware::class);
+
+ if (! $container->bound(ProxyPlan::class) || ! $container->bound(RouteManifest::class)) {
+ return;
+ }
+
+ $rules = [];
+ $plan = $container->make(ProxyPlan::class);
+ foreach ($plan->classes() as $class) {
+ foreach ($plan->methodsFor($class) as $method => $rows) {
+ foreach ($rows as $row) {
+ if ($row['advice'] !== FeatureFlagAdviceSource::ID || ($row['row']['route'] ?? false) !== true || ($row['row']['fallback'] ?? null) !== null) {
+ continue;
+ }
+
+ /** @var array{class: string, method: string, key: string, variant?: string|null, default?: bool, fallback?: string|null, route?: bool} $data */
+ $data = $row['row'];
+ $rules[$class.'::'.$method] = FeatureFlagMethodDescriptor::fromArray($data);
+ }
+ }
+ }
+
+ if ($rules === []) {
+ return;
+ }
+
+ $decisions = $container->make(RouteGateDecisions::class);
+ $routes = [];
+ foreach ($router->getRoutes()->getRoutes() as $route) {
+ if ($route->getDomain() !== null) {
+ continue;
+ }
+ foreach ($route->methods() as $verb) {
+ if (! is_string($verb)) {
+ continue;
+ }
+
+ $routes[$verb.' '.$route->uri()] = $route;
+ }
+ }
+
+ foreach ($container->make(RouteManifest::class)->all() as $descriptor) {
+ $rule = $rules[$descriptor->controllerClass.'::'.$descriptor->methodName] ?? null;
+ $uri = trim($descriptor->path, '/');
+ $route = $routes[strtoupper($descriptor->httpMethod).' '.($uri === '' ? '/' : $uri)] ?? null;
+
+ if ($rule !== null && $route !== null) {
+ $decisions->register($route, $rule);
+ $route->middleware($rule->middleware());
+ }
+ }
+ }
+}
diff --git a/packages/feature-flags/src/Gating/FeatureFlagsGatingConfiguration.php b/packages/feature-flags/src/Gating/FeatureFlagsGatingConfiguration.php
new file mode 100644
index 00000000..f5895e64
--- /dev/null
+++ b/packages/feature-flags/src/Gating/FeatureFlagsGatingConfiguration.php
@@ -0,0 +1,40 @@
+ */
+ private WeakMap $routes;
+
+ public function __construct(private readonly Container $app)
+ {
+ $this->routes = new WeakMap;
+ }
+
+ public function register(Route $route, FeatureFlagMethodDescriptor $rule): void
+ {
+ $this->routes[$route] = $rule;
+ }
+
+ public function record(Request $request, string $key, ?string $variant, bool $default): void
+ {
+ $route = $request->route();
+ if (! $route instanceof Route) {
+ return;
+ }
+
+ $rule = $this->routes[$route] ?? null;
+ if ($rule !== null && $rule->key === $key && $rule->variant === $variant && $rule->default === $default) {
+ $request->attributes->set(self::ATTRIBUTE, $rule);
+ }
+ }
+
+ public function passed(FeatureFlagMethodDescriptor $rule): bool
+ {
+ if (! $this->app->bound('request')) {
+ return false;
+ }
+
+ $request = $this->app->make('request');
+ $route = $request->route();
+ if (! $route instanceof Route) {
+ return false;
+ }
+
+ $routedRule = $this->routes[$route] ?? null;
+
+ return $routedRule !== null
+ && $request->attributes->get(self::ATTRIBUTE) === $routedRule
+ && $routedRule->toArray() === $rule->toArray();
+ }
+}
diff --git a/packages/feature-flags/src/Management/FeatureFlagsHealthIndicator.php b/packages/feature-flags/src/Management/FeatureFlagsHealthIndicator.php
new file mode 100644
index 00000000..dae59caa
--- /dev/null
+++ b/packages/feature-flags/src/Management/FeatureFlagsHealthIndicator.php
@@ -0,0 +1,73 @@
+today = $today ?? static fn (): string => gmdate('Y-m-d');
+ }
+
+ public function available(): bool
+ {
+ return $this->registry() !== null;
+ }
+
+ public function health(): Health
+ {
+ $registry = $this->registry();
+ if ($registry === null) {
+ return Health::unknown();
+ }
+
+ $composition = $registry->composition(false);
+ $sources = [];
+ $down = false;
+ foreach ($registry->states() as $state) {
+ $sources[$state->name] = [
+ 'status' => $state->status(),
+ 'flags' => $state->flags,
+ 'lastRefresh' => $state->lastRefresh,
+ 'error' => $state->error,
+ ];
+ $down = $down || ! $state->loaded;
+ }
+
+ $details = [
+ 'sources' => $sources,
+ 'flags' => count($composition->flags),
+ 'expired' => FlagDefinitions::expiredKeys($composition->document()->flags, ($this->today)()),
+ ];
+
+ return $down ? Health::down($details) : Health::up($details);
+ }
+
+ private function registry(): ?FlagRegistry
+ {
+ $registry = $this->beans->has(FlagRegistry::class) ? $this->beans->get(FlagRegistry::class) : null;
+
+ return $registry instanceof FlagRegistry ? $registry : null;
+ }
+}
diff --git a/packages/feature-flags/src/Management/FeatureFlagsManagementConfiguration.php b/packages/feature-flags/src/Management/FeatureFlagsManagementConfiguration.php
new file mode 100644
index 00000000..4a780936
--- /dev/null
+++ b/packages/feature-flags/src/Management/FeatureFlagsManagementConfiguration.php
@@ -0,0 +1,62 @@
+has(FlagRegistry::class) ? $beans->get(FlagRegistry::class) : null;
+ $writer = $beans->has(FlagStoreWriter::class) ? $beans->get(FlagStoreWriter::class) : null;
+
+ $preview = array_values(array_filter(
+ $beans->getAll(EvaluationContextContributor::class),
+ static fn (object $contributor): bool => $contributor instanceof ApplicationEvaluationContextContributor,
+ ));
+ $actors = array_values(array_filter(
+ $beans->getAll(FlagActorSource::class),
+ static fn (object $source): bool => $source instanceof FlagActorSource,
+ ));
+
+ /** @var list $preview */
+ /** @var list $actors */
+ return new FlagManagement(
+ $settings,
+ $provider,
+ $registry instanceof FlagRegistry ? $registry : null,
+ $writer instanceof FlagStoreWriter ? $writer : null,
+ new EvaluationContextResolver($preview),
+ $actors,
+ logger: $logger ?? new NullLogger,
+ );
+ }
+}
diff --git a/packages/feature-flags/src/Management/FlagActorSource.php b/packages/feature-flags/src/Management/FlagActorSource.php
new file mode 100644
index 00000000..7107125a
--- /dev/null
+++ b/packages/feature-flags/src/Management/FlagActorSource.php
@@ -0,0 +1,15 @@
+ FeatureFlags); without one the caller's fallback is
+ * recorded — `actuator`, `admin` or `cli:` (spec §4.8).
+ */
+interface FlagActorSource
+{
+ public function actor(): ?string;
+}
diff --git a/packages/feature-flags/src/Management/FlagManagement.php b/packages/feature-flags/src/Management/FlagManagement.php
new file mode 100644
index 00000000..694ac275
--- /dev/null
+++ b/packages/feature-flags/src/Management/FlagManagement.php
@@ -0,0 +1,413 @@
+ */
+ public const array WRITE_ACTIONS = ['enable', 'disable', 'default-variant', 'put', 'delete'];
+
+ /** @var Closure(): string */
+ private readonly Closure $today;
+
+ /**
+ * @param list $actors
+ * @param (Closure(): string)|null $today the UTC date, Y-m-d
+ */
+ public function __construct(
+ private readonly FeatureFlagsSettings $settings,
+ private readonly Provider $provider,
+ private readonly ?FlagRegistry $registry = null,
+ private readonly ?FlagStoreWriter $writer = null,
+ private readonly EvaluationContextResolver $preview = new EvaluationContextResolver,
+ private readonly array $actors = [],
+ ?Closure $today = null,
+ private readonly LoggerInterface $logger = new NullLogger,
+ ) {
+ $this->today = $today ?? static fn (): string => gmdate('Y-m-d');
+ }
+
+ public function writable(): bool
+ {
+ return $this->writer !== null;
+ }
+
+ public function writesEnabled(): bool
+ {
+ return $this->settings->writes;
+ }
+
+ /**
+ * `GET /actuator/flags`.
+ *
+ * @return array{provider: array{name: string, status: string}, writable: bool, writesEnabled: bool, sources: list>, flags: list>}
+ */
+ public function overview(): array
+ {
+ $sources = [];
+ $flags = [];
+
+ if ($this->registry !== null) {
+ $composition = $this->registry->composition();
+ foreach ($this->registry->states() as $state) {
+ $sources[] = [
+ 'name' => $state->name,
+ 'enabled' => true,
+ 'status' => $state->status(),
+ 'flags' => $state->flags,
+ 'lastRefresh' => $state->lastRefresh,
+ 'error' => $state->error,
+ 'revision' => $this->registry->source($state->name)?->reportedRevision($state->revision),
+ ];
+ }
+
+ $versions = [];
+ try {
+ foreach ($this->writer?->store()->all() ?? [] as $stored) {
+ $versions[$stored->key] = $stored->version;
+ }
+ } catch (Throwable $failure) {
+ // An operator can still inspect the last-good composition during a store outage.
+ $this->warn('Flag store versions are unavailable for the management overview: {error}.', null, $failure);
+ }
+
+ $today = ($this->today)();
+ foreach ($composition->flags as $flag) {
+ $flags[] = $this->summary($flag, $versions[$flag->definition->key] ?? null, $today);
+ }
+ }
+
+ return [
+ 'provider' => ['name' => $this->provider->getMetadata()->getName(), 'status' => 'READY'],
+ 'writable' => $this->writable(),
+ 'writesEnabled' => $this->writesEnabled(),
+ 'sources' => $sources,
+ 'flags' => $flags,
+ ];
+ }
+
+ /**
+ * `GET /actuator/flags/{key}`.
+ *
+ * @return array{key: string, definition: stdClass|array, origin: string, layers: list}>, version: int|null, expired: bool, history: list}
+ *
+ * @throws FlagManagementException
+ */
+ public function describe(string $key): array
+ {
+ $flag = $this->composed($key);
+ $store = $this->writer?->store();
+ $version = null;
+ $history = [];
+ if ($store !== null) {
+ try {
+ $version = $store->get($key)?->version;
+ $history = array_map(static fn (FlagChange $change): array => $change->toHistoryRow(), $store->history($key, self::HISTORY_LIMIT));
+ } catch (Throwable $failure) {
+ // Store diagnostics must not obscure a visible last-good definition.
+ $this->warn('Flag store diagnostics are unavailable for [{key}]: {error}.', $key, $failure);
+ }
+ }
+
+ return [
+ 'key' => $key,
+ 'definition' => $flag->definition->toJsonValue(),
+ 'origin' => $flag->origin,
+ 'layers' => array_map(
+ static fn (array $layer): array => ['source' => $layer['source'], 'definition' => $layer['definition']->toJsonValue()],
+ $flag->layers,
+ ),
+ 'version' => $version,
+ 'expired' => $flag->definition->isExpired(($this->today)()),
+ 'history' => $history,
+ ];
+ }
+
+ /**
+ * The `evaluate` action: explicit context only (plus application/profiles), no hooks.
+ *
+ * @param array $context
+ * @return array{key: string, value: mixed, variant: ?string, reason: string, errorCode: ?string, metadata: stdClass|array}
+ *
+ * @throws FlagManagementException
+ */
+ public function evaluate(string $key, array $context = [], ?string $targetingKey = null): array
+ {
+ $evaluationContext = $this->preview->resolve($context, $targetingKey, ambient: false);
+
+ if ($this->provider instanceof FireflyFlagProvider && $this->registry !== null) {
+ $type = $this->composed($key)->definition->flagType();
+
+ $default = $type === FlagType::Object ? new stdClass : $type->zero();
+
+ return FlagEvaluation::fromResolution($key, $this->provider->resolution($key, $type, $default, $evaluationContext))->toArray();
+ }
+
+ // External-provider mode: Firefly knows no flag types, so the preview is a boolean evaluation, default false.
+ $details = $this->provider->resolveBooleanValue($key, false, $evaluationContext);
+ $error = $details->getError();
+
+ return (new FlagEvaluation(
+ $key,
+ $details->getValue(),
+ $error === null ? $details->getVariant() : null,
+ $error === null ? ($details->getReason() ?? 'UNKNOWN') : 'ERROR',
+ $error?->getResolutionErrorCode()->getValue(),
+ $error?->getResolutionErrorMessage(),
+ ))->toArray();
+ }
+
+ /**
+ * `POST /actuator/flags/{key}`: `evaluate`, or one of the WRITE_ACTIONS. Answers the key's describe() body
+ * after a write, or `{"key", "deleted": true}` when a delete leaves no layer defining the key.
+ *
+ * @param mixed $body the decoded request body (Json's faithful form, or a plain array from a form)
+ * @param string $fallbackActor recorded when no FlagActorSource names the caller
+ * @return array
+ *
+ * @throws FlagManagementException
+ */
+ public function apply(string $key, mixed $body, string $fallbackActor): array
+ {
+ if (! Json::isObject($body)) {
+ throw new FlagManagementException(ManagementError::BadRequest, 'The request body must be a JSON object naming an action.');
+ }
+
+ $members = Json::members($body);
+ $action = $members['action'] ?? null;
+ if (! is_string($action) || ($action !== 'evaluate' && ! in_array($action, self::WRITE_ACTIONS, true))) {
+ throw new FlagManagementException(ManagementError::BadRequest, 'The action must be one of evaluate, '.implode(', ', self::WRITE_ACTIONS).'.');
+ }
+
+ if ($action === 'evaluate') {
+ return $this->evaluate($key, self::context($members['context'] ?? null), self::targetingKey($members['targetingKey'] ?? null));
+ }
+
+ $writer = $this->writer();
+ $expected = self::expectedVersion($members['expectedVersion'] ?? null);
+ $actor = $this->actor($fallbackActor);
+
+ try {
+ switch ($action) {
+ case 'put':
+ if (! array_key_exists('definition', $members)) {
+ throw new FlagManagementException(ManagementError::BadRequest, 'put needs a definition: one flagd flag object.');
+ }
+ $writer->put($key, $members['definition'], $actor, $expected);
+ break;
+
+ case 'delete':
+ if ($writer->store()->get($key) === null) {
+ throw new FlagManagementException(ManagementError::UnknownFlag, "The store holds no flag [{$key}]; only a stored flag can be deleted.");
+ }
+ $writer->delete($key, $actor, $expected);
+ break;
+
+ default:
+ $writer->put($key, $this->changed($key, $action, $members), $actor, $expected);
+ }
+ } catch (InvalidFlagDefinition $invalid) {
+ throw new FlagManagementException(ManagementError::InvalidDefinition, $invalid->getMessage(), $invalid);
+ } catch (FlagStoreConflict $conflict) {
+ throw new FlagManagementException(ManagementError::Conflict, $conflict->getMessage(), $conflict);
+ }
+
+ try {
+ return $this->describe($key);
+ } catch (Throwable $failure) {
+ $this->warn('Flag management detail is unavailable after an accepted write to [{key}]: {error}.', $key, $failure);
+
+ return ['key' => $key, $action === 'delete' ? 'deleted' : 'refreshPending' => true];
+ }
+ }
+
+ /**
+ * The definition `enable`, `disable` or `default-variant` writes: the stored one, or else the effective one.
+ *
+ * @param array $members
+ * @return array
+ *
+ * @throws FlagManagementException
+ */
+ private function changed(string $key, string $action, array $members): array
+ {
+ $stored = $this->writer?->store()->get($key);
+ $composed = $this->registry?->composition(false)->flag($key);
+ $base = $stored !== null
+ ? FlagDefinition::fromJsonValue($key, $stored->definition)
+ : ($composed === null
+ ? throw new FlagManagementException(ManagementError::UnknownFlag, "No layer defines flag [{$key}].")
+ : $composed->definition);
+
+ $definition = Json::members($base->toJsonValue());
+
+ if ($action === 'default-variant') {
+ $variant = $members['variant'] ?? null;
+ if (! is_string($variant) || $variant === '') {
+ throw new FlagManagementException(ManagementError::BadRequest, 'default-variant needs a variant: the name of one of the flag\'s variants.');
+ }
+ if (! $base->hasVariant($variant)) {
+ throw new FlagManagementException(ManagementError::UnknownVariant, "Flag [{$key}] has no variant [{$variant}]; its variants are ".implode(', ', $base->variantNames()).'.');
+ }
+ $definition['defaultVariant'] = $variant;
+
+ return $definition;
+ }
+
+ $definition['state'] = $action === 'enable' ? 'ENABLED' : 'DISABLED';
+
+ return $definition;
+ }
+
+ /**
+ * @throws FlagManagementException
+ */
+ private function composed(string $key): ComposedFlag
+ {
+ return $this->registry?->composition()->flag($key)
+ ?? throw new FlagManagementException(ManagementError::UnknownFlag, "No layer defines flag [{$key}].");
+ }
+
+ /**
+ * @throws FlagManagementException
+ */
+ private function writer(): FlagStoreWriter
+ {
+ if (! $this->settings->writes) {
+ throw new FlagManagementException(ManagementError::WritesDisabled, 'Flag writes are disabled: set firefly.feature-flags.management.writes to true.');
+ }
+
+ return $this->writer
+ ?? throw new FlagManagementException(ManagementError::NotWritable, 'No flag store is configured: enable firefly.feature-flags.sources.store to make flags writable.');
+ }
+
+ private function actor(string $fallback): string
+ {
+ foreach ($this->actors as $source) {
+ try {
+ $actor = $source->actor();
+ } catch (Throwable) {
+ $actor = null;
+ }
+
+ if ($actor !== null && $actor !== '') {
+ return $actor;
+ }
+ }
+
+ return $fallback;
+ }
+
+ private function warn(string $message, ?string $key, Throwable $failure): void
+ {
+ try {
+ $this->logger->warning($message, ['key' => $key, 'error' => $failure->getMessage()]);
+ } catch (Throwable) {
+ // A diagnostic failure or logger failure cannot undo an accepted write.
+ }
+ }
+
+ /**
+ * @return array
+ */
+ private function summary(ComposedFlag $flag, ?int $version, string $today): array
+ {
+ $definition = $flag->definition;
+
+ return [
+ 'key' => $definition->key,
+ 'state' => $definition->state(),
+ 'type' => $definition->valueType(),
+ 'variants' => $definition->variantNames(),
+ 'defaultVariant' => $definition->defaultVariant(),
+ 'targeting' => $definition->targeting() !== null,
+ 'origin' => $flag->origin,
+ 'overrides' => $flag->overrides,
+ 'metadata' => Json::object($definition->metadata()),
+ 'expired' => $definition->isExpired($today),
+ 'version' => $version,
+ ];
+ }
+
+ /**
+ * @return array
+ *
+ * @throws FlagManagementException
+ */
+ private static function context(mixed $context): array
+ {
+ if ($context === null) {
+ return [];
+ }
+
+ if (! Json::isObject($context)) {
+ throw new FlagManagementException(ManagementError::BadRequest, 'context must be an object of evaluation attributes.');
+ }
+
+ $attributes = [];
+ foreach (Json::members($context) as $name => $value) {
+ $attributes[(string) $name] = Json::toPhp($value);
+ }
+
+ return $attributes;
+ }
+
+ /**
+ * @throws FlagManagementException
+ */
+ private static function targetingKey(mixed $targetingKey): ?string
+ {
+ if ($targetingKey !== null && ! is_string($targetingKey)) {
+ throw new FlagManagementException(ManagementError::BadRequest, 'targetingKey must be a string.');
+ }
+
+ return $targetingKey;
+ }
+
+ /**
+ * @throws FlagManagementException
+ */
+ private static function expectedVersion(mixed $expected): ?int
+ {
+ if ($expected !== null && (! is_int($expected) || $expected < 0)) {
+ throw new FlagManagementException(ManagementError::BadRequest, 'expectedVersion must be a non-negative integer.');
+ }
+
+ return $expected;
+ }
+}
diff --git a/packages/feature-flags/src/Management/FlagManagementException.php b/packages/feature-flags/src/Management/FlagManagementException.php
new file mode 100644
index 00000000..8eec5758
--- /dev/null
+++ b/packages/feature-flags/src/Management/FlagManagementException.php
@@ -0,0 +1,25 @@
+ $this->error->value, 'message' => $this->getMessage()];
+ }
+}
diff --git a/packages/feature-flags/src/Management/FlagsEndpoint.php b/packages/feature-flags/src/Management/FlagsEndpoint.php
new file mode 100644
index 00000000..649ae3c3
--- /dev/null
+++ b/packages/feature-flags/src/Management/FlagsEndpoint.php
@@ -0,0 +1,87 @@
+beans->has(FlagManagement::class);
+ }
+
+ public function handle(EndpointRequest $request): ?EndpointResponse
+ {
+ $management = $this->beans->has(FlagManagement::class) ? $this->beans->get(FlagManagement::class) : null;
+ if (! $management instanceof FlagManagement || count($request->subPath) > 1) {
+ return null;
+ }
+
+ $key = $request->subPath[0] ?? null;
+
+ try {
+ if ($request->method === 'GET') {
+ return self::json($key === null ? $management->overview() : $management->describe($key));
+ }
+
+ if ($request->method === 'POST' && $key !== null) {
+ return self::json($management->apply($key, self::body($request), $request->origin === 'admin' ? 'admin' : 'actuator'));
+ }
+ } catch (FlagManagementException $refused) {
+ return self::json($refused->toArray(), $refused->error->status());
+ }
+
+ return null;
+ }
+
+ /**
+ * @throws FlagManagementException
+ */
+ private static function body(EndpointRequest $request): mixed
+ {
+ if ($request->rawBody === null || trim($request->rawBody) === '') {
+ return $request->body;
+ }
+
+ try {
+ return Json::decode($request->rawBody);
+ } catch (JsonException) {
+ throw new FlagManagementException(ManagementError::BadRequest, 'The request body is not valid JSON.');
+ }
+ }
+
+ /**
+ * @param array $body
+ */
+ private static function json(array $body, int $status = 200): EndpointResponse
+ {
+ return EndpointResponse::text(Json::encode($body), $status, 'application/json');
+ }
+}
diff --git a/packages/feature-flags/src/Management/ManagementError.php b/packages/feature-flags/src/Management/ManagementError.php
new file mode 100644
index 00000000..b4742b88
--- /dev/null
+++ b/packages/feature-flags/src/Management/ManagementError.php
@@ -0,0 +1,31 @@
+ 403,
+ self::NotWritable, self::Conflict => 409,
+ self::InvalidDefinition, self::UnknownVariant => 422,
+ self::UnknownFlag => 404,
+ self::BadRequest => 400,
+ };
+ }
+}
diff --git a/packages/feature-flags/src/Provider/FireflyFlagProvider.php b/packages/feature-flags/src/Provider/FireflyFlagProvider.php
new file mode 100644
index 00000000..0f609fdc
--- /dev/null
+++ b/packages/feature-flags/src/Provider/FireflyFlagProvider.php
@@ -0,0 +1,117 @@
+evaluate($flagKey, FlagType::Boolean, $defaultValue, $context)->toResolutionDetails();
+ }
+
+ public function resolveStringValue(string $flagKey, string $defaultValue, ?EvaluationContext $context = null): ResolutionDetails
+ {
+ return $this->evaluate($flagKey, FlagType::String, $defaultValue, $context)->toResolutionDetails();
+ }
+
+ public function resolveIntegerValue(string $flagKey, int $defaultValue, ?EvaluationContext $context = null): ResolutionDetails
+ {
+ return $this->evaluate($flagKey, FlagType::Integer, $defaultValue, $context)->toResolutionDetails();
+ }
+
+ public function resolveFloatValue(string $flagKey, float $defaultValue, ?EvaluationContext $context = null): ResolutionDetails
+ {
+ return $this->evaluate($flagKey, FlagType::Float, $defaultValue, $context)->toResolutionDetails();
+ }
+
+ /**
+ * @param mixed[] $defaultValue
+ */
+ public function resolveObjectValue(string $flagKey, array $defaultValue, ?EvaluationContext $context = null): ResolutionDetails
+ {
+ return $this->evaluate($flagKey, FlagType::Object, $defaultValue, $context)->toResolutionDetails();
+ }
+
+ /**
+ * The Firefly view of one evaluation, metadata included (what the SDK's ResolutionDetails cannot carry), with a
+ * value that is the caller's own copy. No OpenFeature client is involved, so no hook runs: an evaluation made
+ * here records no metric and no exposure event.
+ */
+ public function resolution(string $flagKey, FlagType $type, mixed $default, ?EvaluationContext $context = null): Resolution
+ {
+ $resolution = $this->evaluate($flagKey, $type, $default, $context);
+
+ return $resolution->withValue(ValueCopy::of($resolution->value));
+ }
+
+ /** The evaluator's answer; callers defensively copy or convert values from custom evaluators too. */
+ private function evaluate(string $flagKey, FlagType $type, mixed $default, ?EvaluationContext $context): Resolution
+ {
+ $resolution = $this->attempt($flagKey, $type, $default, $context);
+
+ if ($resolution->error !== null) {
+ $this->logger?->debug('Feature flag [{flag}] evaluated with {error}: {message}', [
+ 'flag' => $flagKey,
+ 'error' => $resolution->error->value,
+ 'message' => $resolution->errorMessage,
+ ]);
+ }
+
+ return $resolution;
+ }
+
+ private function attempt(string $flagKey, FlagType $type, mixed $default, ?EvaluationContext $context): Resolution
+ {
+ try {
+ $document = $this->documents->document();
+ } catch (Throwable $failure) {
+ return Resolution::error($default, EvaluationError::General, 'The flag set could not be read: '.$failure->getMessage());
+ }
+
+ try {
+ return $this->evaluator->evaluate(
+ $document,
+ $flagKey,
+ $type,
+ $default,
+ $context?->getTargetingKey(),
+ $context?->getAttributes()->toArray() ?? [],
+ );
+ } catch (Throwable $failure) {
+ return Resolution::error($default, EvaluationError::General, "Evaluating flag [{$flagKey}] failed: {$failure->getMessage()}");
+ }
+ }
+}
diff --git a/packages/feature-flags/src/Provider/ValueCopy.php b/packages/feature-flags/src/Provider/ValueCopy.php
new file mode 100644
index 00000000..14a7da5b
--- /dev/null
+++ b/packages/feature-flags/src/Provider/ValueCopy.php
@@ -0,0 +1,153 @@
+ $objects */
+ $objects = new SplObjectStorage;
+
+ $arrays = [];
+
+ return self::copy($value, 1, $objects, $arrays);
+ }
+
+ /** Exposure-only budget: containers and scalars count once per occurrence, including repeated references. */
+ public static function forExposure(mixed $value): mixed
+ {
+ $remaining = 10000;
+ $stack = [self::members([$value])];
+ while ($stack !== []) {
+ $iterator = $stack[array_key_last($stack)];
+ if (! $iterator->valid()) {
+ array_pop($stack);
+
+ continue;
+ }
+ if ($remaining-- === 0) {
+ throw new UnexpectedValueException('Exposure snapshot exceeds 10000 value occurrences');
+ }
+ $member = $iterator->current();
+ $iterator->next();
+ if (is_array($member) || $member instanceof stdClass) {
+ $stack[] = self::members($member);
+ }
+ }
+
+ /** @var SplObjectStorage $objects */
+ $objects = new SplObjectStorage;
+ $arrays = [];
+
+ // Native array equality can expand compact graphs again; the bounded snapshot needs no array memo.
+ return self::copy($value, 1, $objects, $arrays, false);
+ }
+
+ /** @return Generator */
+ private static function members(mixed $value): Generator
+ {
+ if ($value instanceof stdClass) {
+ $value = get_object_vars($value);
+ }
+ if (is_array($value)) {
+ foreach ($value as $member) {
+ yield $member;
+ }
+ }
+ }
+
+ /**
+ * @param SplObjectStorage $objects
+ * @param array, array}>> $arrays
+ */
+ private static function copy(mixed $value, int $depth, SplObjectStorage $objects, array &$arrays, bool $memoizeArrays = true): mixed
+ {
+ if ($value instanceof stdClass && $objects->offsetExists($value)) {
+ return $objects[$value];
+ }
+ if ($depth > self::MAX_DEPTH) {
+ throw new UnexpectedValueException('Flag value exceeds the snapshot depth limit');
+ }
+
+ if ($value instanceof stdClass) {
+ $copy = new stdClass;
+ $objects[$value] = $copy;
+ foreach (get_object_vars($value) as $name => $member) {
+ $copy->{$name} = self::copy($member, $depth + 1, $objects, $arrays, $memoizeArrays);
+ }
+
+ return $copy;
+ }
+
+ if (is_array($value)) {
+ if ($memoizeArrays) {
+ $signature = self::arraySignature($value);
+ foreach ($arrays[$signature] ?? [] as [$original, $snapshot]) {
+ if ($original === $value) {
+ return $snapshot;
+ }
+ }
+ }
+
+ $copy = [];
+ foreach ($value as $key => $member) {
+ if (is_array($member) && ReflectionReference::fromArrayElement($value, $key) !== null) {
+ throw new UnexpectedValueException('Flag value contains unsupported array references');
+ }
+ $copy[$key] = self::copy($member, $depth + 1, $objects, $arrays, $memoizeArrays);
+ }
+ if ($memoizeArrays) {
+ $arrays[$signature][] = [$value, $copy];
+ }
+
+ return $copy;
+ }
+
+ return $value;
+ }
+
+ /** @param array $value */
+ private static function arraySignature(array $value): string
+ {
+ $shape = [];
+ foreach ($value as $key => $member) {
+ if (is_array($member) && ReflectionReference::fromArrayElement($value, $key) !== null) {
+ throw new UnexpectedValueException('Flag value contains unsupported array references');
+ }
+ $shape[] = [$key, get_debug_type($member), match (true) {
+ is_array($member) => count($member),
+ is_object($member) => spl_object_id($member),
+ is_resource($member) => get_resource_id($member),
+ default => $member,
+ }];
+ }
+
+ return hash('sha256', serialize($shape));
+ }
+}
diff --git a/packages/feature-flags/src/Registry/CacheBook.php b/packages/feature-flags/src/Registry/CacheBook.php
new file mode 100644
index 00000000..a2b4fd86
--- /dev/null
+++ b/packages/feature-flags/src/Registry/CacheBook.php
@@ -0,0 +1,142 @@
+cache->get(self::key($key));
+ $this->recovered();
+
+ return $value;
+ } catch (Throwable $failure) {
+ $this->degrade($failure);
+
+ return null;
+ }
+ }
+
+ public function put(string $key, mixed $value): void
+ {
+ try {
+ $this->cache->forever(self::key($key), $value);
+ $this->recovered();
+ } catch (Throwable $failure) {
+ $this->degrade($failure);
+ }
+ }
+
+ /** True for exactly one caller per key and window (Cache::add); every caller while the store is down. */
+ public function first(string $key, int $seconds): bool
+ {
+ try {
+ $first = $this->cache->add(self::key($key), true, max(1, $seconds));
+ $this->recovered();
+
+ return $first;
+ } catch (Throwable $failure) {
+ $this->degrade($failure);
+
+ return true;
+ }
+ }
+
+ /**
+ * A non-blocking lock: the release closure, or null when another process holds it. A store without atomic
+ * locks (or a store that is down) grants a no-op lock rather than refusing every refresh.
+ *
+ * @return (Closure(): void)|null
+ */
+ public function lock(string $key, int $seconds): ?Closure
+ {
+ try {
+ $store = $this->cache->getStore();
+ if (! $store instanceof LockProvider) {
+ $this->recovered();
+
+ return static function (): void {};
+ }
+
+ $lock = $store->lock(self::key($key), max(1, $seconds));
+ $acquired = $lock->get() === true;
+ $this->recovered();
+
+ return $acquired ? function () use ($lock): void {
+ try {
+ $lock->release();
+ } catch (Throwable $failure) {
+ $this->degrade($failure); // the lock expires on its own; the refresh that held it stands
+ }
+ } : null;
+ } catch (Throwable $failure) {
+ $this->degrade($failure);
+
+ return static function (): void {};
+ }
+ }
+
+ public function degraded(): bool
+ {
+ return $this->degraded;
+ }
+
+ /** The full cache key: every entry of the bookkeeping lives under PREFIX. */
+ private static function key(string $key): string
+ {
+ return self::PREFIX.$key;
+ }
+
+ private function recovered(): void
+ {
+ $this->degraded = false;
+ $this->warned = false;
+ }
+
+ private function degrade(Throwable $failure): void
+ {
+ $this->degraded = true;
+ if ($this->warned) {
+ return;
+ }
+
+ $this->warned = true;
+ $this->logger->warning('Feature flags cannot reach the cache store ({error}); each process loads its flag sources itself until the store is back.', ['error' => $failure->getMessage()]);
+ }
+}
diff --git a/packages/feature-flags/src/Registry/ComposedFlag.php b/packages/feature-flags/src/Registry/ComposedFlag.php
new file mode 100644
index 00000000..86b2cde8
--- /dev/null
+++ b/packages/feature-flags/src/Registry/ComposedFlag.php
@@ -0,0 +1,22 @@
+ $overrides the lower layers that also define the key, lowest first
+ * @param list $layers every layer defining the key, lowest first
+ */
+ public function __construct(
+ public FlagDefinition $definition,
+ public string $origin,
+ public array $overrides,
+ public array $layers,
+ ) {}
+}
diff --git a/packages/feature-flags/src/Registry/Composer.php b/packages/feature-flags/src/Registry/Composer.php
new file mode 100644
index 00000000..a940f638
--- /dev/null
+++ b/packages/feature-flags/src/Registry/Composer.php
@@ -0,0 +1,48 @@
+ $layers source name and document, lowest precedence first
+ */
+ public static function compose(array $layers): Composition
+ {
+ $byKey = [];
+ $evaluators = [];
+ $metadata = [];
+
+ foreach ($layers as [$source, $document]) {
+ foreach ($document->flags as $definition) {
+ $byKey[$definition->key][] = ['source' => $source, 'definition' => $definition];
+ }
+ foreach ($document->evaluators as $name => $rule) {
+ $evaluators[$name] = $rule;
+ }
+ $metadata = array_replace($metadata, $document->metadata);
+ }
+
+ $flags = [];
+ foreach ($byKey as $key => $layersOfKey) {
+ $winner = $layersOfKey[array_key_last($layersOfKey)];
+ $flags[(string) $key] = new ComposedFlag(
+ $winner['definition'],
+ $winner['source'],
+ array_map(static fn (array $layer): string => $layer['source'], array_slice($layersOfKey, 0, -1)),
+ $layersOfKey,
+ );
+ }
+ ksort($flags, SORT_STRING);
+
+ return new Composition($flags, $evaluators, $metadata);
+ }
+}
diff --git a/packages/feature-flags/src/Registry/Composition.php b/packages/feature-flags/src/Registry/Composition.php
new file mode 100644
index 00000000..f51e6a85
--- /dev/null
+++ b/packages/feature-flags/src/Registry/Composition.php
@@ -0,0 +1,128 @@
+ $flags keyed by flag key, sorted as text
+ * @param array $evaluators
+ * @param array $metadata
+ */
+ public function __construct(
+ public array $flags = [],
+ public array $evaluators = [],
+ public array $metadata = [],
+ ) {}
+
+ public function flag(string $key): ?ComposedFlag
+ {
+ return $this->flags[$key] ?? null;
+ }
+
+ public function document(): FlagDocument
+ {
+ $definitions = [];
+ foreach ($this->flags as $flag) {
+ $definitions[$flag->definition->key] = $flag->definition;
+ }
+
+ return new FlagDocument($definitions, $this->evaluators, $this->metadata);
+ }
+
+ /**
+ * Flag key => sha256 of everything an evaluation of that key reads, sorted by key as text: the whole composed
+ * definition (fields flagd does not read included), the metadata it reports (the document's, overridden key by key
+ * by its own) and every evaluator its targeting references, directly or through another evaluator (a name no
+ * evaluator has counts as absent). Values compare as JSON does: `true` is not `1` and `1` is not `1.0`. Two
+ * compositions answer a key differently only if its fingerprint differs, so FeatureFlagsChanged names exactly the
+ * keys whose fingerprint moved. A numeric-looking key is an int key: read it with `(string) $key`.
+ *
+ * @return array
+ *
+ * @throws \JsonException as Json::encode() (never for a validated document)
+ */
+ public function fingerprints(): array
+ {
+ $fingerprints = [];
+ foreach ($this->flags as $flag) {
+ $definition = $flag->definition;
+ $fingerprints[$definition->key] = hash('sha256', Json::canonical([
+ 'definition' => $definition->toJsonValue(),
+ 'metadata' => Json::object(array_replace($this->metadata, $definition->metadata())),
+ 'evaluators' => Json::object($this->evaluatorsUsedBy($definition)),
+ ]));
+ }
+ ksort($fingerprints, SORT_STRING);
+
+ return $fingerprints;
+ }
+
+ /**
+ * The evaluators $definition's targeting depends on, transitively: name => rule, null for a name no evaluator has.
+ * References resolve inside targeting only (spec §4.1), so a variant that looks like one is a value.
+ *
+ * @return array
+ */
+ private function evaluatorsUsedBy(FlagDefinition $definition): array
+ {
+ $used = [];
+ $pending = self::references($definition->targeting());
+ while ($pending !== []) {
+ $name = array_pop($pending);
+ if (array_key_exists($name, $used)) {
+ continue;
+ }
+ $rule = $this->evaluators[$name] ?? null;
+ $used[$name] = $rule;
+ if ($rule !== null) {
+ array_push($pending, ...self::references($rule));
+ }
+ }
+
+ return $used;
+ }
+
+ /**
+ * Every evaluator name $value references: a `{"$ref": name}` object with exactly one member, a text value, at any
+ * depth (an object that also holds other members is not a reference). Walks with a stack of its own.
+ *
+ * @return list
+ */
+ private static function references(mixed $value): array
+ {
+ $names = [];
+ $pending = [$value];
+ while ($pending !== []) {
+ $node = array_pop($pending);
+ if ($node instanceof stdClass) {
+ $node = get_object_vars($node);
+ }
+ if (! is_array($node)) {
+ continue;
+ }
+ if (count($node) === 1 && is_string($node['$ref'] ?? null)) {
+ $names[] = $node['$ref'];
+
+ continue;
+ }
+ foreach ($node as $child) {
+ $pending[] = $child;
+ }
+ }
+
+ return $names;
+ }
+}
diff --git a/packages/feature-flags/src/Registry/FlagDocumentSource.php b/packages/feature-flags/src/Registry/FlagDocumentSource.php
new file mode 100644
index 00000000..a554a8da
--- /dev/null
+++ b/packages/feature-flags/src/Registry/FlagDocumentSource.php
@@ -0,0 +1,13 @@
+ */
+ private readonly array $sources;
+
+ /** @var Closure(): float */
+ private readonly Closure $clock;
+
+ private ?Composition $shared = null;
+
+ /** The shared composition with the test layer on top; null without test overrides. */
+ private ?Composition $effective = null;
+
+ /** The effective composition's document, built once per composition. */
+ private ?FlagDocument $document = null;
+
+ /** @var list the layers of the shared composition, lowest precedence first */
+ private array $layers = [];
+
+ /** @var list those layers as source name and document JSON (unchanged = same composition) */
+ private array $signature = [];
+
+ private float $memoUntil = 0.0;
+
+ private ?FlagDocument $testOverrides = null;
+
+ /** @var array what this process last knew, for when the cache store knows nothing */
+ private array $states = [];
+
+ /** @var array per source, the last document JSON decoded and its document */
+ private array $documents = [];
+
+ /** The last composition could not be recorded (another worker held the record lock): record it at the next refresh. */
+ private bool $recordPending = false;
+
+ /**
+ * @param iterable $sources
+ * @param (Closure(): float)|null $clock Unix seconds
+ *
+ * @throws ConfigurationException two sources share a name, or a source takes a name the registry reserves
+ */
+ public function __construct(
+ iterable $sources,
+ private readonly CacheBook $book,
+ private readonly ApplicationEventPublisher $events,
+ private readonly LoggerInterface $logger = new NullLogger,
+ ?Closure $clock = null,
+ ) {
+ $ordered = [...$sources];
+ usort($ordered, static fn (FlagSource $a, FlagSource $b): int => $a->precedence() <=> $b->precedence());
+
+ $names = [];
+ foreach ($ordered as $source) {
+ $name = $source->name();
+ if (in_array($name, [self::TEST_LAYER, self::TEST_OVERRIDES_ORIGIN, self::STARTUP_ORIGIN, self::RECORD], true)) {
+ throw new ConfigurationException(sprintf('A feature flag source cannot be named [%s]: the registry reserves %s, %s and %s for its own origins and %s for its shared record.', $name, self::TEST_LAYER, self::TEST_OVERRIDES_ORIGIN, self::STARTUP_ORIGIN, self::RECORD));
+ }
+ if (isset($names[$name])) {
+ throw new ConfigurationException(sprintf('Two feature flag sources are named [%s]: every source needs a name of its own (it keys the source\'s shared state).', $name));
+ }
+ $names[$name] = true;
+ }
+
+ $this->sources = $ordered;
+ $this->clock = $clock ?? static fn (): float => microtime(true);
+ }
+
+ /**
+ * Boot: check what is due, then warn once a UTC day across every worker per expired flag.
+ *
+ * @throws InvalidFlagDefinition the configuration, or a flag file that never loaded, breaks a rule: names the
+ * flag key, the reason and the source
+ * @throws FlagSourceUnavailable a flag file that never loaded cannot be read
+ */
+ public function start(): void
+ {
+ $composition = $this->shared(refresh: true);
+ $today = gmdate('Y-m-d', (int) floor(($this->clock)()));
+ $definitions = array_map(static fn (ComposedFlag $flag) => $flag->definition, $composition->flags);
+
+ foreach (FlagDefinitions::expiredKeys($definitions, $today) as $key) {
+ $flag = $composition->flag($key);
+ if ($flag !== null && $this->book->first('expired:'.$today.':'.$key, 86400)) {
+ $this->logger->warning('Feature flag [{key}] expired on {expires} and is still defined by [{origin}]: remove it, or move its expires date.', [
+ 'key' => $key,
+ 'expires' => $flag->definition->expires(),
+ 'origin' => $flag->origin,
+ ]);
+ }
+ }
+ }
+
+ /** The effective document the provider evaluates: every source, test overrides on top. */
+ public function document(): FlagDocument
+ {
+ $composition = $this->composition();
+
+ return $this->document ??= $composition->document();
+ }
+
+ /**
+ * @param bool $includeTestOverrides false for what this process serves to others (sync endpoint, health)
+ */
+ public function composition(bool $includeTestOverrides = true): Composition
+ {
+ $shared = $this->shared();
+
+ return $includeTestOverrides ? ($this->effective ?? $shared) : $shared;
+ }
+
+ /**
+ * Check every source that is due (or forced), compose, and announce a change of the effective set.
+ *
+ * @param bool $force re-check sources now, even before their refresh-interval elapsed
+ * @param string|null $only with $force: re-check only this source (a store write refreshes the store alone)
+ * @return Composition without test overrides
+ */
+ public function refresh(bool $force = false, ?string $only = null): Composition
+ {
+ $now = ($this->clock)();
+ $loaded = [];
+ $layers = [];
+ $moved = [];
+
+ foreach ($this->sources as $source) {
+ $name = $source->name();
+ $before = $this->state($name);
+ $forced = $force && ($only === null || $only === $name);
+ $deferred = $source instanceof StoreFlagSource && $source->transactionActive();
+ $after = ! $deferred && ($forced || $this->due($source, $before, $now)) ? $this->check($source, $before, $now, $forced) : $before;
+ $document = $after === null ? null : $this->decoded($after);
+
+ if (! $deferred && $after !== null && $after->loaded && $document === null) {
+ // A cached entry this process cannot read (corrupt, or written in another shape): unknown, so reload it.
+ $this->logger->warning('The cached document of feature flag source [{source}] cannot be read; reloading the source.', ['source' => $name]);
+ $after = $this->check($source, null, $now, forced: true, known: false);
+ $document = $this->decoded($after);
+ }
+ if ($after === null || $after->document === null || $document === null) {
+ continue; // a source that never loaded contributes nothing
+ }
+ if ($before === null || $before->document !== $after->document) {
+ $moved[] = $name;
+ }
+ $this->states[$name] = $after; // the newest document this process serves, loaded here or adopted from the cache
+ $loaded[] = [$name, $after->document];
+ $layers[] = [$name, $document];
+ }
+
+ $this->memoUntil = $now + $this->memoSeconds();
+
+ $shared = $this->shared;
+ if ($shared === null || $loaded !== $this->signature || $this->recordPending) {
+ $overridden = $this->testOverrides === null ? null : $this->effective;
+ $shared = Composer::compose($layers);
+ $this->layers = $layers;
+ $this->signature = $loaded;
+ $this->shared = $shared;
+ $this->effective = $this->testOverrides === null ? null : Composer::compose([...$layers, [self::TEST_LAYER, $this->testOverrides]]);
+ $this->document = null;
+ $this->observe($shared, $loaded, $this->origin($moved, $loaded), $overridden);
+ }
+
+ return $shared;
+ }
+
+ /**
+ * Every registered source's state, in precedence order (a source that never ran yet reports DOWN).
+ *
+ * @return list
+ */
+ public function states(): array
+ {
+ return array_map(fn (FlagSource $source): SourceState => $this->state($source->name()) ?? SourceState::initial($source->name()), $this->sources);
+ }
+
+ /**
+ * @return list
+ */
+ public function sources(): array
+ {
+ return $this->sources;
+ }
+
+ public function source(string $name): ?FlagSource
+ {
+ foreach ($this->sources as $source) {
+ if ($source->name() === $name) {
+ return $source;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * The test-support layer above every source (FeatureFlagOverrides), replaced as a whole; null removes it. It is
+ * in-process only, never shared or served. Publishes FeatureFlagsChanged(keys, 'test-overrides') for the keys
+ * whose effective flag it changes, and nothing when it changes none.
+ */
+ public function overrideForTests(?FlagDocument $overrides): void
+ {
+ $shared = $this->shared();
+ $before = ($this->effective ?? $shared)->fingerprints();
+
+ $this->testOverrides = $overrides;
+ $this->effective = $overrides === null ? null : Composer::compose([...$this->layers, [self::TEST_LAYER, $overrides]]);
+ $this->document = null;
+
+ $changed = self::changedKeys($before, ($this->effective ?? $shared)->fingerprints());
+ if ($changed !== []) {
+ $this->publish(new FeatureFlagsChanged($changed, self::TEST_OVERRIDES_ORIGIN));
+ }
+ }
+
+ public function testOverrides(): ?FlagDocument
+ {
+ return $this->testOverrides;
+ }
+
+ /** The shared composition, refreshed first when there is none yet, its memo expired, or $refresh asks. */
+ private function shared(bool $refresh = false): Composition
+ {
+ $shared = $this->shared;
+ if ($refresh || $shared === null || ($this->clock)() >= $this->memoUntil) {
+ return $this->refresh();
+ }
+
+ return $shared;
+ }
+
+ /**
+ * The cache's state of the source, else this process's own (a store outage, an evicted entry). A cached state that
+ * never loaded does not displace this process's loaded one: it is an evicted entry written back by a worker that
+ * could not load the source, and the last good document stays in use (and is written back at the next check).
+ */
+ private function state(string $name): ?SourceState
+ {
+ $cached = SourceState::fromArray($this->book->get('source:'.$name));
+ $own = $this->states[$name] ?? null;
+ if ($cached === null || $cached->name !== $name) {
+ return $own;
+ }
+
+ return ! $cached->loaded && $own !== null && $own->loaded ? $own : $cached;
+ }
+
+ /**
+ * The state's document, decoded once per JSON text; null when it has none or cannot be read: not JSON, or another
+ * shape than the one SourceState::withLoaded() writes (its flag count disagrees).
+ */
+ private function decoded(SourceState $state): ?FlagDocument
+ {
+ $json = $state->loaded ? $state->document : null;
+ if ($json === null) {
+ return null;
+ }
+
+ $memo = $this->documents[$state->name] ?? null;
+ if ($memo !== null && $memo[0] === $json) {
+ return $memo[1];
+ }
+
+ try {
+ $document = FlagDocument::fromJson($json);
+ } catch (Throwable) {
+ return null;
+ }
+ if (count($document->flags) !== $state->flags) {
+ return null;
+ }
+
+ $this->documents[$state->name] = [$json, $document];
+
+ return $document;
+ }
+
+ private function due(FlagSource $source, ?SourceState $state, float $now): bool
+ {
+ if ($state === null) {
+ return true;
+ }
+
+ $interval = $source->refreshInterval();
+
+ return $interval <= 0.0 || $now - $state->checkedAt >= $interval;
+ }
+
+ /**
+ * Load the source and record the result in the cache and in this process. A polling source loads under its lock:
+ * the holder re-reads the state once it has the lock (another worker may have checked it in between) and writes
+ * the new state before letting the lock go, so the next holder reads this check instead of loading again.
+ *
+ * @param bool $known false when the cached state cannot be read: load from scratch, ignoring it
+ *
+ * @throws InvalidFlagDefinition|FlagSourceUnavailable|Throwable as load()
+ */
+ private function check(FlagSource $source, ?SourceState $before, float $now, bool $forced, bool $known = true): SourceState
+ {
+ $name = $source->name();
+ $interval = $source->refreshInterval();
+ if ($interval <= 0.0) {
+ return $this->settle($name, $before, $this->load($source, $before ?? SourceState::initial($name), $now), everyRefresh: true);
+ }
+
+ $release = $this->book->lock('lock:'.$name, (int) ceil($interval) + 1);
+ if ($release === null) {
+ if ($before !== null && ! $forced) {
+ return $before; // another worker is checking it: serve the last good document meanwhile
+ }
+
+ return $this->settle($name, $before, $this->load($source, $before ?? SourceState::initial($name), $now));
+ }
+
+ try {
+ $latest = $known ? ($this->state($name) ?? $before) : null;
+ if (! $forced && $latest !== null && ! $this->due($source, $latest, $now)) {
+ return $latest; // another worker checked it between this one reading its state and taking the lock
+ }
+
+ return $this->settle($name, $latest, $this->load($source, $latest ?? SourceState::initial($name), $now));
+ } finally {
+ $release();
+ }
+ }
+
+ /**
+ * Remember a check in this process and in the cache. A source checked on every refresh that found nothing new
+ * writes nothing: its check time tells nothing, and a cache write per request would.
+ */
+ private function settle(string $name, ?SourceState $before, SourceState $after, bool $everyRefresh = false): SourceState
+ {
+ if ($everyRefresh && $before !== null && $after->sameAs($before)) {
+ $this->states[$name] = $before;
+
+ return $before;
+ }
+
+ $this->states[$name] = $after;
+ $this->book->put('source:'.$name, $after->toArray());
+
+ return $after;
+ }
+
+ /**
+ * @throws InvalidFlagDefinition|FlagSourceUnavailable|Throwable only when the failure refuses the boot: always for
+ * configuration, for a flag file until it ever loaded
+ */
+ private function load(FlagSource $source, SourceState $before, float $now): SourceState
+ {
+ $name = $source->name();
+
+ try {
+ $snapshot = $source->load($before->loaded ? $before->revision : null);
+
+ return $snapshot === null ? $before->withUnchanged($now) : $before->withLoaded($snapshot->document, $snapshot->revision, $now);
+ } catch (Throwable $failure) {
+ if ($source->failsStartup() && ($name === FlagSource::CONFIG || ! $before->loaded)) {
+ throw $failure instanceof InvalidFlagDefinition ? $failure->fromSource($name) : $failure;
+ }
+
+ $error = $failure->getMessage() !== '' ? $failure->getMessage() : $failure::class;
+ if ($before->error !== $error) {
+ $this->logger->warning('Feature flag source [{source}] failed{kept}: {error}', [
+ 'source' => $name,
+ 'kept' => $before->loaded ? '; its last good document stays in use' : '',
+ 'error' => $error,
+ ]);
+ }
+
+ return $before->withFailure($error, $now);
+ }
+
+ }
+
+ /**
+ * Record the shared composition for every worker, then announce what changed for this process.
+ *
+ * Without test overrides that is the shared transition, announced once across all workers. While test overrides
+ * are active the application sees the effective set, so the keys are those whose EFFECTIVE flag changed in this
+ * process: a source change to a key an override shadows is not announced (clearing the override announces it),
+ * and an overridden key whose targeting references a changed evaluator is.
+ *
+ * @param list $loaded the composed layers as source name and document JSON
+ * @param Composition|null $overridden the effective composition before this refresh, while test overrides are active
+ */
+ private function observe(Composition $shared, array $loaded, string $origin, ?Composition $overridden): void
+ {
+ $transition = $this->record($shared, $loaded, hash('sha256', implode("\0", array_merge(...$loaded))));
+
+ if ($overridden !== null && $this->effective !== null) {
+ $changed = self::changedKeys($overridden->fingerprints(), $this->effective->fingerprints());
+ if ($changed !== []) {
+ $this->publish(new FeatureFlagsChanged($changed, $origin));
+ }
+
+ return;
+ }
+
+ if ($transition !== null && $this->book->first($transition['guard'], self::CHANGE_WINDOW)) {
+ $this->publish(new FeatureFlagsChanged($transition['changed'], $transition['startup'] ? self::STARTUP_ORIGIN : $origin));
+ }
+ }
+
+ /**
+ * Compare the shared composition with the last one the shared bookkeeping recorded and record it: the transition
+ * to announce (its keys, whether it is the first record, the guard key that lets one worker announce it), or null
+ * when there is none, the store is down, or this process composed documents older than the shared ones.
+ *
+ * The record only moves forward. It is read, compared and written under the non-blocking lock `lock:composed`
+ * (a worker that finds it held skips: the holder records, or this process at its next refresh, or the next
+ * request), and right before writing a transition the shared source states are read again: a worker whose layers
+ * no longer match them (it served a last good document while another worker loaded a newer one) neither writes
+ * nor announces — it would announce a revert, and the next request the change again. The common case — the same
+ * layers as the record — is answered from one read, without the lock.
+ *
+ * @param list $loaded the composed layers as source name and document JSON
+ * @param string $layersHash sha256 of $loaded: the same layers compose the same set, so the common case
+ * (nothing moved since another worker recorded it) fingerprints no flag
+ * @return array{changed: non-empty-list, startup: bool, guard: string}|null
+ */
+ private function record(Composition $shared, array $loaded, string $layersHash): ?array
+ {
+ $previous = self::composedRecord($this->book->get(self::RECORD));
+ if ($this->book->degraded()) {
+ return null; // no shared memory of the previous set: every process would announce a "change"
+ }
+ if ($previous !== null && $previous['layers'] === $layersHash) {
+ $this->recordPending = false;
+
+ return null; // the common case, checked without the lock
+ }
+
+ $release = $this->book->lock('lock:'.self::RECORD, self::RECORD_LOCK);
+ if ($release === null) {
+ $this->recordPending = true; // another worker is recording: it, or this process's next refresh, records
+
+ return null;
+ }
+
+ try {
+ $this->recordPending = false;
+ $previous = self::composedRecord($this->book->get(self::RECORD));
+ if ($previous !== null && $previous['layers'] === $layersHash) {
+ return null;
+ }
+
+ $keys = $shared->fingerprints();
+ $fingerprint = hash('sha256', Json::canonical(Json::object($keys)));
+ if ($previous !== null && $previous['fingerprint'] === $fingerprint) {
+ $this->book->put(self::RECORD, [...$previous, 'layers' => $layersHash]); // other documents, the same effective flags
+
+ return null;
+ }
+ if (! $this->current($loaded)) {
+ return null;
+ }
+
+ // The generation makes each transition's guard key unique, so flipping back and forth is announced every time.
+ $generation = ($previous['generation'] ?? 0) + 1;
+ $this->book->put(self::RECORD, ['generation' => $generation, 'layers' => $layersHash, 'fingerprint' => $fingerprint, 'keys' => $keys]);
+
+ $changed = self::changedKeys($previous['keys'] ?? [], $keys);
+
+ return $changed === [] ? null : ['changed' => $changed, 'startup' => $previous === null, 'guard' => 'changed:'.$generation.':'.$fingerprint];
+ } finally {
+ $release();
+ }
+ }
+
+ /**
+ * Whether the source states hold, right now, the documents this process composed.
+ *
+ * @param list $loaded
+ */
+ private function current(array $loaded): bool
+ {
+ $states = [];
+ foreach ($this->sources as $source) {
+ $state = $this->state($source->name());
+ if ($state !== null && $state->loaded && $state->document !== null) {
+ $states[] = [$source->name(), $state->document];
+ }
+ }
+
+ return $states === $loaded;
+ }
+
+ /**
+ * The highest-precedence source whose document moved, else the highest-precedence loaded source (the change
+ * came from another process or deploy), else the highest-precedence source.
+ *
+ * @param list $moved
+ * @param list $loaded
+ */
+ private function origin(array $moved, array $loaded): string
+ {
+ if ($moved !== []) {
+ return $moved[count($moved) - 1];
+ }
+ if ($loaded !== []) {
+ return $loaded[count($loaded) - 1][0];
+ }
+
+ return $this->sources === [] ? self::STARTUP_ORIGIN : $this->sources[count($this->sources) - 1]->name();
+ }
+
+ private function publish(FeatureFlagsChanged $event): void
+ {
+ try {
+ $this->events->publish($event);
+ } catch (Throwable $failure) {
+ $this->logger->warning('A FeatureFlagsChanged listener failed ({error}); the flags it announced stay in force.', ['error' => $failure->getMessage()]);
+ }
+ }
+
+ /** Seconds until a polling source may be due again: its interval (0 = every read); configuration never is. */
+ private function memoSeconds(): float
+ {
+ $seconds = INF;
+ foreach ($this->sources as $source) {
+ if ($source->name() !== FlagSource::CONFIG) {
+ $seconds = min($seconds, max(0.0, $source->refreshInterval()));
+ }
+ }
+
+ return $seconds;
+ }
+
+ /**
+ * The keys whose fingerprint differs between two Composition::fingerprints() maps, added and removed keys
+ * included, sorted as text.
+ *
+ * @param array $before
+ * @param array $after
+ * @return list
+ */
+ private static function changedKeys(array $before, array $after): array
+ {
+ $changed = [];
+ foreach ([...array_keys($before), ...array_keys($after)] as $key) {
+ if (($before[$key] ?? null) !== ($after[$key] ?? null)) {
+ $changed[(string) $key] = (string) $key;
+ }
+ }
+ $changed = array_values($changed);
+ sort($changed, SORT_STRING);
+
+ return $changed;
+ }
+
+ /**
+ * The shared record observe() writes, or null for anything else.
+ *
+ * @return array{generation: int, layers: string, fingerprint: string, keys: array}|null
+ */
+ private static function composedRecord(mixed $record): ?array
+ {
+ if (! is_array($record)
+ || ! is_int($record['generation'] ?? null)
+ || ! is_string($record['layers'] ?? null)
+ || ! is_string($record['fingerprint'] ?? null)
+ || ! is_array($record['keys'] ?? null)) {
+ return null;
+ }
+
+ $keys = [];
+ foreach ($record['keys'] as $key => $fingerprint) {
+ if (! is_string($fingerprint)) {
+ return null;
+ }
+ $keys[$key] = $fingerprint;
+ }
+
+ return ['generation' => $record['generation'], 'layers' => $record['layers'], 'fingerprint' => $record['fingerprint'], 'keys' => $keys];
+ }
+}
diff --git a/packages/feature-flags/src/Registry/SourceState.php b/packages/feature-flags/src/Registry/SourceState.php
new file mode 100644
index 00000000..937928dc
--- /dev/null
+++ b/packages/feature-flags/src/Registry/SourceState.php
@@ -0,0 +1,124 @@
+ $this->name,
+ 'loaded' => $this->loaded,
+ 'document' => $this->document,
+ 'revision' => $this->revision,
+ 'checkedAt' => $this->checkedAt,
+ 'lastRefresh' => $this->lastRefresh,
+ 'error' => $this->error,
+ 'flags' => $this->flags,
+ ];
+ }
+
+ /** @throws \JsonException if the document cannot be serialized and read back */
+ public function withLoaded(FlagDocument $document, ?string $revision, float $now): self
+ {
+ $json = $document->toJson();
+ FlagDocument::fromJson($json);
+
+ return new self($this->name, true, $json, $revision, $now, self::timestamp($now), null, count($document->flags));
+ }
+
+ /** A successful check that found nothing new: it refreshed, and any earlier error is over. */
+ public function withUnchanged(float $now): self
+ {
+ return new self($this->name, $this->loaded, $this->document, $this->revision, $now, self::timestamp($now), null, $this->flags);
+ }
+
+ public function withFailure(string $error, float $now): self
+ {
+ return new self($this->name, $this->loaded, $this->document, $this->revision, $now, $this->lastRefresh, $error, $this->flags);
+ }
+
+ /** The same source, document, revision and error: only the check and refresh times may differ. */
+ public function sameAs(self $other): bool
+ {
+ return $this->name === $other->name
+ && $this->loaded === $other->loaded
+ && $this->document === $other->document
+ && $this->revision === $other->revision
+ && $this->error === $other->error;
+ }
+
+ /** The last good document; an empty one when the source never loaded. */
+ public function document(): FlagDocument
+ {
+ return $this->document === null ? new FlagDocument : FlagDocument::fromJson($this->document);
+ }
+
+ public function status(): string
+ {
+ return match (true) {
+ ! $this->loaded => 'DOWN',
+ $this->error !== null => 'STALE',
+ default => 'UP',
+ };
+ }
+
+ private static function timestamp(float $now): string
+ {
+ return gmdate('Y-m-d\TH:i:s\Z', (int) floor($now));
+ }
+}
diff --git a/packages/feature-flags/src/Scanner/FeatureFlagScanner.php b/packages/feature-flags/src/Scanner/FeatureFlagScanner.php
new file mode 100644
index 00000000..2a1884f6
--- /dev/null
+++ b/packages/feature-flags/src/Scanner/FeatureFlagScanner.php
@@ -0,0 +1,332 @@
+ $psr4 namespace-prefix => absolute directory
+ * @return array>> class => method => row
+ */
+ public function scanProxyAdvice(array $psr4): array
+ {
+ $advice = [];
+ foreach ($this->scan($psr4) as $rule) {
+ $advice[$rule->class][$rule->method] = $rule->toArray();
+ }
+
+ foreach ($advice as $class => $methods) {
+ ksort($methods);
+ $advice[$class] = $methods;
+ }
+ ksort($advice);
+
+ /** @var array>> $advice */
+ return $advice;
+ }
+
+ /**
+ * @param array $psr4
+ * @return list
+ */
+ public function scan(array $psr4): array
+ {
+ $classes = $this->classes($psr4);
+ $produced = $this->beanFactoryTypes($classes);
+
+ $compiled = [];
+ foreach ($classes as $class) {
+ $rules = $this->compile($class);
+ if ($rules !== []) {
+ $compiled[$class] = $rules;
+ }
+ }
+
+ $rules = [];
+ foreach ($compiled as $class => $classRules) {
+ $reflection = new ReflectionClass($class);
+ $classFlag = $reflection->getAttributes(FeatureFlag::class) !== [];
+
+ if ($this->postProcessed($reflection, $produced)) {
+ if ($reflection->isFinal()) {
+ throw new ConfigurationException("#[FeatureFlag] on {$class} cannot be applied: the class is final and a proxy must extend it. Remove `final`, or gate the call with FeatureFlags::isEnabled() where it is made.");
+ }
+ $rules = [...$rules, ...$classRules];
+
+ continue;
+ }
+
+ $covered = $this->coveredBySubclasses($class, $classes, $produced, $compiled);
+ foreach ($classRules as $rule) {
+ if (! $classFlag && $reflection->getMethod($rule->method)->getDeclaringClass()->getName() !== $class) {
+ continue;
+ }
+
+ if (! isset($covered[$rule->method])) {
+ throw new ConfigurationException("#[FeatureFlag] on {$rule->site()} cannot be applied: nothing post-processes {$class} — it has no #[Component]-family stereotype, no #[Bean] method returns it and no post-processed subclass compiles a row for {$rule->method}() — so no proxy would ever run the gate. Add a stereotype such as #[Service], or gate the call with FeatureFlags::isEnabled().");
+ }
+ }
+ }
+
+ return $rules;
+ }
+
+ /**
+ * @param class-string $class
+ * @return list
+ */
+ private function compile(string $class): array
+ {
+ $reflection = new ReflectionClass($class);
+ $this->refuseAbstractAncestorRule($reflection);
+ $classFlag = $this->first($reflection->getAttributes(FeatureFlag::class));
+ $route = $reflection->getAttributes(RestController::class, ReflectionAttribute::IS_INSTANCEOF) !== [];
+ $fallbacks = $this->fallbackTargets($reflection, $classFlag);
+
+ $rules = [];
+ foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
+ $own = $this->first($method->getAttributes(FeatureFlag::class));
+ $site = $class.'::'.$method->getName();
+
+ if ($method->isStatic() || $method->isConstructor() || str_starts_with($method->getName(), '__')) {
+ if ($own !== null) {
+ throw new ConfigurationException("#[FeatureFlag] on {$site} cannot be applied: ".($method->isStatic() ? 'a static call has no instance for a proxy to wrap' : 'a constructor or `__` method is never routed through the advice chain').'. Move the attribute to a public instance method, or gate the call with FeatureFlags::isEnabled().');
+ }
+
+ continue;
+ }
+
+ $flag = $own ?? (isset($fallbacks[strtolower($method->getName())]) ? null : $classFlag);
+ if ($flag === null) {
+ continue;
+ }
+
+ if ($method->isFinal()) {
+ if ($own === null && $method->getDeclaringClass()->getName() !== $class) {
+ continue;
+ }
+
+ throw new ConfigurationException("#[FeatureFlag] on {$site} cannot be applied: the method is final and a proxy must override it. Remove `final` (a class-level #[FeatureFlag] applies to every public method, this one included).");
+ }
+
+ $this->assertFlag($flag, $site);
+ if ($flag->fallback !== null) {
+ $this->assertFallback($reflection, $method, $flag->fallback, $site);
+ }
+
+ $rules[] = new FeatureFlagMethodDescriptor($class, $method->getName(), $flag->key, $flag->variant, $flag->default, $flag->fallback, $route);
+ }
+
+ return $rules;
+ }
+
+ private function assertFlag(FeatureFlag $flag, string $site): void
+ {
+ if (preg_match(FlagDefinitions::KEY_PATTERN, $flag->key) !== 1) {
+ throw new ConfigurationException("#[FeatureFlag] on {$site} names [{$flag->key}], which is not a valid flag key (^[A-Za-z0-9][A-Za-z0-9._-]{0,127}\$).");
+ }
+
+ if ($flag->variant !== null && ($flag->variant === '' || str_contains($flag->variant, ','))) {
+ throw new ConfigurationException("#[FeatureFlag] on {$site} gates on variant [{$flag->variant}]: a variant used by a gate must be non-empty and contain no comma.");
+ }
+ }
+
+ /**
+ * @param ReflectionClass $reflection
+ */
+ private function assertFallback(ReflectionClass $reflection, ReflectionMethod $gated, string $fallback, string $site): void
+ {
+ if (strcasecmp($fallback, $gated->getName()) === 0) {
+ throw new ConfigurationException("#[FeatureFlag] on {$site} names the gated method itself as its fallback, which would answer by calling the gate again. Name a DIFFERENT method that returns the degraded answer.");
+ }
+
+ if (! $reflection->hasMethod($fallback)) {
+ throw new ConfigurationException("#[FeatureFlag] on {$site} names fallback [{$fallback}], which {$reflection->getName()} does not declare.");
+ }
+
+ $recovery = $reflection->getMethod($fallback);
+ if (! $recovery->isPublic() || $recovery->isStatic()) {
+ throw new ConfigurationException("#[FeatureFlag] on {$site} names fallback [{$fallback}], which is ".($recovery->isStatic() ? 'static' : 'not public').'. The proxy calls the fallback on the bean from outside the class: make it a public instance method.');
+ }
+
+ if ($recovery->getNumberOfRequiredParameters() > $gated->getNumberOfParameters()) {
+ throw new ConfigurationException("#[FeatureFlag] on {$site} names fallback [{$fallback}], which requires {$recovery->getNumberOfRequiredParameters()} arguments; the gated call supplies {$gated->getNumberOfParameters()}. Give the fallback the gated method's signature.");
+ }
+ }
+
+ /**
+ * Methods some #[FeatureFlag] on this class names as its fallback: the class-level attribute skips them.
+ *
+ * @param ReflectionClass $reflection
+ * @return array
+ */
+ private function fallbackTargets(ReflectionClass $reflection, ?FeatureFlag $classFlag): array
+ {
+ $targets = [];
+ if ($classFlag?->fallback !== null) {
+ $targets[strtolower($classFlag->fallback)] = true;
+ }
+
+ foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
+ $fallback = $this->first($method->getAttributes(FeatureFlag::class))?->fallback;
+ if ($fallback !== null && strcasecmp($fallback, $method->getName()) !== 0) {
+ $targets[strtolower($fallback)] = true;
+ }
+ }
+
+ return $targets;
+ }
+
+ /**
+ * The methods every post-processed subclass of $class compiles a row for (an inherited method attribute IS
+ * visible on the subclass; a class attribute is not inherited by PHP).
+ *
+ * @param list $classes
+ * @param array $produced
+ * @param array> $compiled
+ * @return array
+ */
+ private function coveredBySubclasses(string $class, array $classes, array $produced, array $compiled): array
+ {
+ $covered = null;
+ foreach ($classes as $candidate) {
+ if ($candidate === $class || ! is_subclass_of($candidate, $class) || ! $this->postProcessed(new ReflectionClass($candidate), $produced)) {
+ continue;
+ }
+
+ $own = [];
+ foreach ($compiled[$candidate] ?? [] as $rule) {
+ $own[$rule->method] = true;
+ }
+ $covered = $covered === null ? $own : array_intersect_key($covered, $own);
+ }
+
+ return $covered ?? [];
+ }
+
+ /**
+ * @param ReflectionClass $reflection
+ * @param array $produced
+ */
+ private function postProcessed(ReflectionClass $reflection, array $produced): bool
+ {
+ return $reflection->getAttributes(Component::class, ReflectionAttribute::IS_INSTANCEOF) !== []
+ || isset($produced[$reflection->getName()]);
+ }
+
+ /**
+ * @param list $classes
+ * @return array
+ */
+ private function beanFactoryTypes(array $classes): array
+ {
+ $types = [];
+ foreach ($classes as $class) {
+ $reflection = new ReflectionClass($class);
+ if ($reflection->getAttributes(Component::class, ReflectionAttribute::IS_INSTANCEOF) === []) {
+ continue;
+ }
+
+ foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
+ $returns = $method->getReturnType();
+ if ($method->getAttributes(Bean::class) !== [] && $returns instanceof ReflectionNamedType && ! $returns->isBuiltin() && class_exists($returns->getName())) {
+ $types[$returns->getName()] = true;
+ }
+ }
+ }
+
+ return $types;
+ }
+
+ /**
+ * @param list> $attributes
+ */
+ private function first(array $attributes): ?FeatureFlag
+ {
+ return $attributes === [] ? null : $attributes[0]->newInstance();
+ }
+
+ /** @param ReflectionClass $reflection */
+ private function refuseAbstractAncestorRule(ReflectionClass $reflection): void
+ {
+ for ($parent = $reflection->getParentClass(); $parent !== false; $parent = $parent->getParentClass()) {
+ if ($parent->isAbstract() && $parent->getAttributes(FeatureFlag::class) !== []) {
+ throw new ConfigurationException("#[FeatureFlag] on {$parent->getName()} cannot be applied: a class-level #[FeatureFlag] is not inherited by {$reflection->getName()}, so no proxy could enforce it. Put the attribute on the concrete service or its methods.");
+ }
+ }
+ }
+
+ /**
+ * Concrete classes under the PSR-4 roots, sorted (the ResilienceMethodScanner walk).
+ *
+ * @param array $psr4
+ * @return list
+ */
+ private function classes(array $psr4): array
+ {
+ $classes = [];
+ foreach ($psr4 as $prefix => $directory) {
+ if (! is_dir($directory)) {
+ continue;
+ }
+ $prefix = rtrim($prefix, '\\').'\\';
+ $root = rtrim((string) realpath($directory), DIRECTORY_SEPARATOR);
+ /** @var iterable $files */
+ $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root, RecursiveDirectoryIterator::SKIP_DOTS));
+ foreach ($files as $file) {
+ if (! $file->isFile() || $file->getExtension() !== 'php') {
+ continue;
+ }
+ $relative = substr((string) $file->getRealPath(), strlen($root) + 1, -4);
+ $class = $prefix.str_replace(DIRECTORY_SEPARATOR, '\\', $relative);
+ if (! class_exists($class)) {
+ continue;
+ }
+ $reflection = new ReflectionClass($class);
+ if (! $reflection->isAbstract() && ! $reflection->isInterface()) {
+ $classes[] = $class;
+ }
+ }
+ }
+ sort($classes);
+
+ return $classes;
+ }
+}
diff --git a/packages/feature-flags/src/Server/FlagdSyncController.php b/packages/feature-flags/src/Server/FlagdSyncController.php
new file mode 100644
index 00000000..dab892d8
--- /dev/null
+++ b/packages/feature-flags/src/Server/FlagdSyncController.php
@@ -0,0 +1,42 @@
+settings->server->token;
+ if ($token !== '' && ! hash_equals($token, (string) $request->bearerToken())) {
+ return new Response('', 401, ['WWW-Authenticate' => 'Bearer realm="feature-flags"', 'Cache-Control' => 'no-cache']);
+ }
+
+ $body = $this->registry->composition(false)->document()->toJson();
+ $etag = '"'.hash('sha256', $body).'"';
+ $headers = ['ETag' => $etag, 'Cache-Control' => 'no-cache'];
+
+ $match = array_map(static function (string $value): string {
+ $value = trim($value);
+
+ return str_starts_with($value, 'W/') ? substr($value, 2) : $value;
+ }, explode(',', (string) $request->headers->get('If-None-Match', '')));
+ if (in_array($etag, $match, true) || in_array('*', $match, true)) {
+ return new Response('', 304, $headers);
+ }
+
+ return new Response($body, 200, ['Content-Type' => 'application/json'] + $headers);
+ }
+}
diff --git a/packages/feature-flags/src/Server/FlagdSyncRouteRegistrar.php b/packages/feature-flags/src/Server/FlagdSyncRouteRegistrar.php
new file mode 100644
index 00000000..f8ad7207
--- /dev/null
+++ b/packages/feature-flags/src/Server/FlagdSyncRouteRegistrar.php
@@ -0,0 +1,49 @@
+container;
+ if (! $container->bound(FeatureFlagsSettings::class) || ! $container->bound(FlagRegistry::class)) {
+ return;
+ }
+
+ /** @var FeatureFlagsSettings $settings */
+ $settings = $container->make(FeatureFlagsSettings::class);
+ if (! $settings->server->enabled) {
+ return;
+ }
+
+ /** @var Router $router */
+ $router = $container->make('router');
+ $router->get($settings->server->path, static fn (Request $request): Response => $container->make(FlagdSyncController::class)($request))
+ ->name(self::ROUTE_NAME);
+ }
+}
diff --git a/packages/feature-flags/src/Settings/DurationReader.php b/packages/feature-flags/src/Settings/DurationReader.php
new file mode 100644
index 00000000..91397d23
--- /dev/null
+++ b/packages/feature-flags/src/Settings/DurationReader.php
@@ -0,0 +1,61 @@
+get('firefly.feature-flags.sources.file.refresh-interval',
+ * 5.0)`. That literal is both the key read and the key every refusal names, so the two cannot drift apart. The
+ * method is called get() on purpose: tests/ConfigReferenceTest.php and DocsCodeAudit::keyLiterals() find a key by
+ * a `->get('firefly.…')` call, so each duration key stays visible to both.
+ *
+ * Accepted: a duration in the framework grammar (`'500ms'`, `'5s'`, `'PT1M'`, see Duration::parse()) or a finite
+ * number of seconds. Null is unset, as everywhere in Config: an absent key, or `env('X')` with X unset, gives the
+ * default. Refused: a negative value, a non-finite number, an empty or unparsable string and any other type; and
+ * zero too when the caller says why zero is unsafe for that key (`$refuseZeroBecause`, quoted in the refusal).
+ *
+ * @internal
+ */
+final readonly class DurationReader
+{
+ public function __construct(private Config $config) {}
+
+ public function get(string $key, float $default, ?string $refuseZeroBecause = null): float
+ {
+ $value = $this->config->get($key);
+ if ($value === null) {
+ return $default;
+ }
+
+ if (is_int($value) || is_float($value)) {
+ $seconds = (float) $value;
+ if (! is_finite($seconds)) {
+ throw new ConfigurationException("Configuration key [{$key}] must be a finite number of seconds, got [".var_export($value, true).'].');
+ }
+ } elseif (is_string($value) && trim($value) !== '') {
+ try {
+ $seconds = Duration::parse($value);
+ } catch (ConfigurationException $exception) {
+ throw new ConfigurationException("Configuration key [{$key}] is not a duration this framework can parse. {$exception->getMessage()}", previous: $exception);
+ }
+ } else {
+ throw new ConfigurationException("Configuration key [{$key}] must be a duration such as '5s', '500ms' or 'PT1M', or a number of seconds, got ".get_debug_type($value).'.');
+ }
+
+ if ($seconds < 0.0) {
+ throw new ConfigurationException("Configuration key [{$key}] must not be negative, got [".var_export($value, true).'].');
+ }
+ if ($refuseZeroBecause !== null && $seconds <= 0.0) {
+ throw new ConfigurationException("Configuration key [{$key}] must be greater than zero, got [".var_export($value, true)."]: {$refuseZeroBecause}.");
+ }
+
+ return $seconds;
+ }
+}
diff --git a/packages/feature-flags/src/Settings/FileSourceSettings.php b/packages/feature-flags/src/Settings/FileSourceSettings.php
new file mode 100644
index 00000000..9d93ef55
--- /dev/null
+++ b/packages/feature-flags/src/Settings/FileSourceSettings.php
@@ -0,0 +1,15 @@
+settings->flags, $this->settings->evaluators]));
+ if ($revision === $knownRevision) {
+ return null;
+ }
+
+ $document = FlagDefinitions::parseDocument([
+ 'flags' => FlagDefinitions::normalize($this->settings->flags),
+ '$evaluators' => $this->settings->evaluators,
+ ]);
+
+ return new SourceSnapshot($document, $revision);
+ }
+}
diff --git a/packages/feature-flags/src/Source/FileFlagSource.php b/packages/feature-flags/src/Source/FileFlagSource.php
new file mode 100644
index 00000000..90882380
--- /dev/null
+++ b/packages/feature-flags/src/Source/FileFlagSource.php
@@ -0,0 +1,177 @@
+`. A file that does not parse is
+ * refused as a whole with the key ``, and the reason names the file.
+ *
+ * Symfony Yaml reads only `true`/`false` as booleans, as YAML 1.2 and flagd do, so `on`/`off` stay variant names.
+ * An unquoted date value (`expires: 2025-01-01`) becomes the text flagd expects, not the int timestamp Symfony Yaml
+ * would give, and a timestamp becomes ISO-8601 text the way PyFly writes one (seconds, microseconds only when
+ * non-zero, the offset only when the file wrote one). A PHP tag (`!php/const`, `!php/object`) is refused instead of
+ * read as null.
+ *
+ * Known divergences from PyFly, all YAML-only (CONTRACT.md: quote dates in YAML; JSON cannot express them):
+ * - An impossible calendar date written unquoted as a value (`expires: 2025-02-30`) reaches this class already rolled
+ * over by PHP's DateTimeImmutable (`2025-03-02`; `2025-00-00` gives `2024-11-30`), so it loads as that real date.
+ * PyFly's YAML parser refuses the file, and the same text in JSON is a string that `expires` validation refuses.
+ * Nothing after parsing can tell, so quote dates: `expires: '2025-02-30'` is refused as it should be.
+ * - An unquoted date as a block-mapping KEY (`2024-01-01: x`) reaches this class as the int key 1704067200 —
+ * Symfony Yaml evaluates block keys without the parse flags, so nothing tells it apart from a key written
+ * `1704067200:` — and is read as the text "1704067200" where PyFly refuses the key. As a flow-mapping key
+ * (`{2024-01-01: x}`) it is refused as invalid YAML. Quote date keys.
+ *
+ * The revision is `mtime-size` at whole seconds (PHP's stat()): an edit that keeps the size and lands within the
+ * same second as the previous one goes unseen until the mtime or the size moves again. The revision is read before
+ * the contents, so it never claims newer bytes than the ones parsed.
+ */
+#[Component]
+#[ConditionalOnProperty(name: 'firefly.feature-flags.enabled', havingValue: 'true')]
+#[ConditionalOnProperty(name: 'firefly.feature-flags.sources.file.enabled', havingValue: 'true')]
+#[ConditionalOnMissingBean(Provider::class)]
+final class FileFlagSource implements FlagSource
+{
+ private const int YAML_FLAGS = Yaml::PARSE_OBJECT_FOR_MAP | Yaml::PARSE_DATETIME | Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE;
+
+ public function __construct(private readonly FeatureFlagsSettings $settings) {}
+
+ public function name(): string
+ {
+ return self::FILE;
+ }
+
+ public function precedence(): int
+ {
+ return 200;
+ }
+
+ public function refreshInterval(): float
+ {
+ return $this->settings->file->refreshInterval;
+ }
+
+ public function failsStartup(): bool
+ {
+ return true;
+ }
+
+ public function reportedRevision(?string $revision): ?string
+ {
+ return $revision;
+ }
+
+ public function load(?string $knownRevision): ?SourceSnapshot
+ {
+ $path = $this->settings->file->path;
+ $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
+ if (! in_array($extension, ['json', 'yaml', 'yml'], true)) {
+ throw new FlagSourceUnavailable("The flag file [{$path}] must end in .json, .yaml or .yml.");
+ }
+
+ clearstatcache(true, $path);
+ $stat = is_file($path) ? @stat($path) : false;
+ if ($stat === false) {
+ throw new FlagSourceUnavailable("The flag file [{$path}] does not exist or cannot be read.");
+ }
+
+ $revision = $stat['mtime'].'-'.$stat['size'];
+ if ($revision === $knownRevision) {
+ return null;
+ }
+
+ $contents = @file_get_contents($path);
+ if ($contents === false) {
+ throw new FlagSourceUnavailable("The flag file [{$path}] cannot be read.");
+ }
+
+ $document = self::decode($path, $extension, $contents) ?? new stdClass; // only an absent document is empty
+
+ return new SourceSnapshot(FlagDefinitions::parseDocument($document), $revision);
+ }
+
+ /**
+ * The document as JSON would decode it; null for an absent one.
+ *
+ * @throws InvalidFlagDefinition the file does not parse
+ */
+ private static function decode(string $path, string $extension, string $contents): mixed
+ {
+ if ($extension === 'json') {
+ try {
+ return Json::decode($contents);
+ } catch (JsonException $failure) {
+ throw self::unparsable($path, 'JSON', $failure);
+ }
+ }
+
+ try {
+ $parsed = Yaml::parse($contents, self::YAML_FLAGS);
+ } catch (Throwable $failure) {
+ // Not only the documented ParseException: Symfony Yaml stores a block-mapping key as a stdClass property,
+ // and a key starting with "\0" throws an Error. Anything parsing throws means the file is not valid YAML.
+ throw self::unparsable($path, 'YAML', $failure);
+ }
+
+ return Json::normalize(self::datesAsText($parsed));
+ }
+
+ private static function unparsable(string $path, string $format, Throwable $failure): InvalidFlagDefinition
+ {
+ return new InvalidFlagDefinition('', sprintf('the flag file [%s] is not valid %s (%s)', $path, $format, rtrim($failure->getMessage(), '.')));
+ }
+
+ /** Every YAML date or timestamp in $value replaced by its text, at any depth. */
+ private static function datesAsText(mixed $value): mixed
+ {
+ if ($value instanceof DateTimeInterface) {
+ return self::isoText($value);
+ }
+
+ if ($value instanceof stdClass) {
+ // through an array: a member name starting with "\0" (a flow-mapping key) cannot be assigned as a property
+ return (object) array_map(self::datesAsText(...), get_object_vars($value));
+ }
+
+ return is_array($value) ? array_map(self::datesAsText(...), $value) : $value;
+ }
+
+ /**
+ * `2025-01-01` for a date; otherwise Python's isoformat(), which PyFly writes: `2025-01-01T10:30:00`, with
+ * `.ffffff` when there are microseconds and the offset when the file wrote one. Symfony Yaml places a timestamp
+ * written without an offset in the zone named "UTC" (an offset written as `Z` or `+00:00` keeps that name), and
+ * reads a date as such a timestamp at midnight.
+ */
+ private static function isoText(DateTimeInterface $at): string
+ {
+ $offsetWritten = $at->getTimezone()->getName() !== 'UTC';
+ if (! $offsetWritten && $at->format('H:i:s.u') === '00:00:00.000000') {
+ return $at->format('Y-m-d');
+ }
+
+ return $at->format('Y-m-d\TH:i:s')
+ .($at->format('u') === '000000' ? '' : $at->format('.u'))
+ .($offsetWritten ? $at->format('P') : '');
+ }
+}
diff --git a/packages/feature-flags/src/Source/FlagSource.php b/packages/feature-flags/src/Source/FlagSource.php
new file mode 100644
index 00000000..23595419
--- /dev/null
+++ b/packages/feature-flags/src/Source/FlagSource.php
@@ -0,0 +1,50 @@
+settings->http->refreshInterval;
+ }
+
+ public function failsStartup(): bool
+ {
+ return false;
+ }
+
+ public function reportedRevision(?string $revision): ?string
+ {
+ return $revision;
+ }
+
+ public function load(?string $knownRevision): ?SourceSnapshot
+ {
+ $settings = $this->settings->http;
+ $request = $this->http->timeout($settings->timeout)->connectTimeout($settings->timeout)->acceptJson();
+ if ($settings->token !== '') {
+ $request = $request->withToken($settings->token);
+ }
+ if ($knownRevision !== null) {
+ $request = $request->withHeaders(['If-None-Match' => $knownRevision]);
+ }
+
+ try {
+ $response = $request->get($settings->url);
+ } catch (Throwable $failure) {
+ throw new FlagSourceUnavailable("The sync endpoint [{$settings->url}] did not answer: {$failure->getMessage()}", 0, $failure);
+ }
+
+ if ($response->status() === 304 && $knownRevision !== null) {
+ return null;
+ }
+ if (! $response->successful()) {
+ throw new FlagSourceUnavailable("The sync endpoint [{$settings->url}] answered HTTP {$response->status()}.");
+ }
+
+ $body = $response->body();
+ try {
+ $document = FlagDefinitions::parseDocument(Json::decode($body));
+ } catch (JsonException $failure) {
+ throw new InvalidFlagDefinition('', 'the flag document is not valid JSON ('.$failure->getMessage().')');
+ }
+
+ $etag = $response->header('ETag');
+
+ return new SourceSnapshot($document, $etag !== '' ? $etag : '"'.hash('sha256', $body).'"');
+ }
+}
diff --git a/packages/feature-flags/src/Source/SourceSnapshot.php b/packages/feature-flags/src/Source/SourceSnapshot.php
new file mode 100644
index 00000000..eb80a798
--- /dev/null
+++ b/packages/feature-flags/src/Source/SourceSnapshot.php
@@ -0,0 +1,16 @@
+settings->store->refreshInterval;
+ }
+
+ public function failsStartup(): bool
+ {
+ return false;
+ }
+
+ public function reportedRevision(?string $revision): ?string
+ {
+ return $revision;
+ }
+
+ public function transactionActive(): bool
+ {
+ try {
+ $store = $this->store();
+ } catch (Throwable) {
+ return false;
+ }
+
+ return $store instanceof TransactionAwareFlagStore && $store->transactionActive();
+ }
+
+ public function load(?string $knownRevision): ?SourceSnapshot
+ {
+ $store = $this->store();
+ if ($store instanceof TransactionAwareFlagStore && $store->transactionActive()) {
+ return null;
+ }
+
+ try {
+ $revision = (string) $store->revision();
+ if ($revision === $knownRevision) {
+ return null;
+ }
+
+ $flags = [];
+ foreach ($store->all() as $stored) {
+ $flags[$stored->key] = $stored->definition;
+ }
+ } catch (Throwable $failure) {
+ throw new FlagSourceUnavailable('The flag store cannot be read: '.$failure->getMessage(), 0, $failure);
+ }
+
+ return new SourceSnapshot(FlagDefinitions::parseDocument(['flags' => Json::object($flags)]), $revision);
+ }
+
+ private function store(): FlagStore
+ {
+ $store = $this->beans->get(FlagStore::class);
+ if (! $store instanceof FlagStore) {
+ throw new FlagSourceUnavailable('No FlagStore bean is configured.');
+ }
+
+ return $store;
+ }
+}
diff --git a/packages/feature-flags/src/Store/CommitAwareFlagStore.php b/packages/feature-flags/src/Store/CommitAwareFlagStore.php
new file mode 100644
index 00000000..d7ac0b06
--- /dev/null
+++ b/packages/feature-flags/src/Store/CommitAwareFlagStore.php
@@ -0,0 +1,14 @@
+connection = $connection;
+ $this->callbacks = new StoreCommitCallbacks($connection);
+ $this->clock = $clock ?? static fn (): DateTimeImmutable => new DateTimeImmutable('now', new DateTimeZone('UTC'));
+ }
+
+ public function transactionActive(): bool
+ {
+ return $this->connection->transactionLevel() > 0;
+ }
+
+ public function afterCommit(Closure $callback): void
+ {
+ $this->callbacks->afterCommit($callback);
+ }
+
+ public function all(): array
+ {
+ $flags = [];
+ foreach ($this->connection->table(FeatureFlagSchema::FLAGS)->useWritePdo()->orderBy('flag_key')->get() as $row) {
+ $flag = self::storedFlag($row);
+ $flags[$flag->key] = $flag;
+ }
+
+ return $flags;
+ }
+
+ public function get(string $key): ?StoredFlag
+ {
+ $row = $this->connection->table(FeatureFlagSchema::FLAGS)->useWritePdo()->where('flag_key', $key)->first();
+
+ return $row === null ? null : self::storedFlag($row);
+ }
+
+ public function revision(): int
+ {
+ $max = $this->connection->table(FeatureFlagSchema::CHANGES)->useWritePdo()->max('id');
+
+ return is_numeric($max) ? (int) $max : 0;
+ }
+
+ public function put(string $key, array $definition, ?string $actor, ?int $expectedVersion = null): FlagChange
+ {
+ $definition = Json::members(Json::decode(self::encode($key, $definition)));
+
+ $attempts = $expectedVersion === null && ! $this->transactionActive() ? 3 : 1;
+ for ($attempt = 1; ; $attempt++) {
+ $rowRaced = false;
+ try {
+ return $this->putAttempt($key, $definition, $actor, $expectedVersion, $rowRaced);
+ } catch (FlagStoreConflict|QueryException $error) {
+ if ($expectedVersion === 0 && $rowRaced && $error instanceof FlagStoreConflict) {
+ throw new FlagStoreConflict($key, $expectedVersion, $this->get($key)?->version);
+ }
+ if (! $rowRaced || $attempts === 1) {
+ throw $error;
+ }
+ if ($attempt >= $attempts) {
+ throw new FlagStoreConflict($key, $expectedVersion, null);
+ }
+ }
+ }
+ }
+
+ /**
+ * @param array $definition
+ */
+ private function putAttempt(string $key, array $definition, ?string $actor, ?int $expectedVersion, bool &$rowRaced): FlagChange
+ {
+ return $this->connection->transaction(function () use ($key, $definition, $actor, $expectedVersion, &$rowRaced): FlagChange {
+ // Create-only writes let the primary key arbitrate absence without InnoDB gap locks.
+ $current = match ($expectedVersion) {
+ 0 => null,
+ null => $this->get($key),
+ default => $this->locked($key),
+ };
+ FlagStoreConflict::check($key, $expectedVersion, $current?->version);
+
+ $now = ($this->clock)();
+ $encoded = self::encode($key, $definition);
+ $stamp = self::stamp($now);
+
+ try {
+ if ($current === null) {
+ try {
+ $this->connection->table(FeatureFlagSchema::FLAGS)->insert([
+ 'flag_key' => $key, 'definition' => $encoded, 'version' => 1, 'updated_at' => $stamp, 'updated_by' => $actor,
+ ]);
+ } catch (UniqueConstraintViolationException) {
+ $rowRaced = true;
+ throw new FlagStoreConflict($key, $expectedVersion, null);
+ }
+ } else {
+ $updated = $this->connection->table(FeatureFlagSchema::FLAGS)
+ ->where('flag_key', $key)
+ ->where('version', $current->version)
+ ->update(['definition' => $encoded, 'version' => $current->version + 1, 'updated_at' => $stamp, 'updated_by' => $actor]);
+ if ($updated !== 1) {
+ $rowRaced = true;
+ throw new FlagStoreConflict($key, $expectedVersion ?? $current->version, null);
+ }
+ }
+ } catch (QueryException $error) {
+ // MariaDB snapshot isolation reports a changed row as ER_CHECKREAD, aborting the transaction.
+ $rowRaced = $this->connection->getDriverName() === 'mysql' && ($error->errorInfo[1] ?? null) === 1020;
+ if ($rowRaced) {
+ $this->callbacks->nativeTransactionAborted($error);
+ }
+ throw $error;
+ }
+
+ return $this->record($key, FlagChange::PUT, $definition, $current?->definition, $actor, $now);
+ });
+ }
+
+ public function delete(string $key, ?string $actor, ?int $expectedVersion = null): ?FlagChange
+ {
+ /** @var FlagChange|null */
+ return $this->connection->transaction(function () use ($key, $actor, $expectedVersion): ?FlagChange {
+ $current = $this->locked($key);
+ FlagStoreConflict::check($key, $expectedVersion, $current?->version);
+ if ($current === null) {
+ return null;
+ }
+
+ $deleted = $this->connection->table(FeatureFlagSchema::FLAGS)->where('flag_key', $key)->where('version', $current->version)->delete();
+ if ($deleted !== 1) {
+ throw new FlagStoreConflict($key, $expectedVersion ?? $current->version, null);
+ }
+
+ return $this->record($key, FlagChange::DELETE, null, $current->definition, $actor, ($this->clock)());
+ });
+ }
+
+ public function history(string $key, int $limit = 50): array
+ {
+ if ($limit <= 0) {
+ return [];
+ }
+ $changes = [];
+ $rows = $this->connection->table(FeatureFlagSchema::CHANGES)->useWritePdo()->where('flag_key', $key)->orderByDesc('id')->limit(max(0, $limit))->get();
+ foreach ($rows as $row) {
+ $changes[] = new FlagChange(
+ self::int($row, 'id'),
+ self::string($row, 'flag_key'),
+ self::string($row, 'action'),
+ self::definition($row, 'definition'),
+ self::definition($row, 'previous'),
+ self::nullableString($row, 'actor'),
+ self::instant(self::string($row, 'changed_at')),
+ );
+ }
+
+ return $changes;
+ }
+
+ private function locked(string $key): ?StoredFlag
+ {
+ $row = $this->connection->table(FeatureFlagSchema::FLAGS)->where('flag_key', $key)->lockForUpdate()->first();
+
+ return $row === null ? null : self::storedFlag($row);
+ }
+
+ /**
+ * @param array|null $definition
+ * @param array|null $previous
+ */
+ private function record(string $key, string $action, ?array $definition, ?array $previous, ?string $actor, DateTimeImmutable $now): FlagChange
+ {
+ $id = $this->connection->table(FeatureFlagSchema::CHANGES)->insertGetId([
+ 'flag_key' => $key,
+ 'action' => $action,
+ 'definition' => $definition === null ? null : self::encode($key, $definition),
+ 'previous' => $previous === null ? null : self::encode($key, $previous),
+ 'actor' => $actor,
+ 'changed_at' => self::stamp($now),
+ ]);
+
+ return new FlagChange((int) $id, $key, $action, $definition, $previous, $actor, $now);
+ }
+
+ /**
+ * @param array $definition
+ */
+ private static function encode(string $key, array $definition): string
+ {
+ return Json::canonical((new FlagDefinition($key, $definition))->toJsonValue());
+ }
+
+ private static function stamp(DateTimeImmutable $now): string
+ {
+ return $now->setTimezone(new DateTimeZone('UTC'))->format('Y-m-d H:i:s.u');
+ }
+
+ private static function instant(string $value): DateTimeImmutable
+ {
+ return new DateTimeImmutable($value, new DateTimeZone('UTC'));
+ }
+
+ private static function storedFlag(object $row): StoredFlag
+ {
+ return new StoredFlag(
+ self::string($row, 'flag_key'),
+ self::definition($row, 'definition') ?? [],
+ self::int($row, 'version'),
+ self::instant(self::string($row, 'updated_at')),
+ self::nullableString($row, 'updated_by'),
+ );
+ }
+
+ /**
+ * @return array|null
+ */
+ private static function definition(object $row, string $column): ?array
+ {
+ $value = self::nullableString($row, $column);
+
+ return $value === null ? null : Json::members(Json::decode($value));
+ }
+
+ private static function string(object $row, string $column): string
+ {
+ $value = get_object_vars($row)[$column] ?? null;
+
+ return is_scalar($value) ? (string) $value : '';
+ }
+
+ private static function nullableString(object $row, string $column): ?string
+ {
+ $value = get_object_vars($row)[$column] ?? null;
+
+ return is_scalar($value) ? (string) $value : null;
+ }
+
+ private static function int(object $row, string $column): int
+ {
+ $value = get_object_vars($row)[$column] ?? null;
+
+ return is_numeric($value) ? (int) $value : 0;
+ }
+}
diff --git a/packages/feature-flags/src/Store/FeatureFlagSchema.php b/packages/feature-flags/src/Store/FeatureFlagSchema.php
new file mode 100644
index 00000000..f7f2769b
--- /dev/null
+++ b/packages/feature-flags/src/Store/FeatureFlagSchema.php
@@ -0,0 +1,75 @@
+getConnection();
+ $collation = null;
+ if ($connection instanceof MySqlConnection) {
+ $modern = version_compare($connection->getServerVersion(), $connection->isMaria() ? '10.2.2' : '8.0.1', '>=');
+ $collation = $modern ? ($connection->isMaria() ? 'utf8mb4_nopad_bin' : 'utf8mb4_0900_bin') : 'utf8mb4_bin';
+ }
+ if (! $schema->hasTable(self::FLAGS)) {
+ $schema->create(self::FLAGS, static function (Blueprint $table) use ($collation): void {
+ $key = $table->string('flag_key', 128)->primary();
+ if ($collation !== null) {
+ $key->charset('utf8mb4')->collation($collation);
+ }
+ $table->longText('definition');
+ $table->integer('version');
+ if ($collation !== null) {
+ $table->dateTime('updated_at', 6);
+ } else {
+ $table->timestamp('updated_at', 6);
+ }
+ $table->string('updated_by', 255)->nullable();
+ });
+ }
+
+ if (! $schema->hasTable(self::CHANGES)) {
+ $schema->create(self::CHANGES, static function (Blueprint $table) use ($collation): void {
+ $table->bigIncrements('id');
+ $key = $table->string('flag_key', 128);
+ if ($collation !== null) {
+ $key->charset('utf8mb4')->collation($collation);
+ }
+ $table->string('action', 16);
+ $table->longText('definition')->nullable();
+ $table->longText('previous')->nullable();
+ $table->string('actor', 255)->nullable();
+ if ($collation !== null) {
+ $table->dateTime('changed_at', 6);
+ } else {
+ $table->timestamp('changed_at', 6);
+ }
+ $table->index(['flag_key', 'id'], self::CHANGES_INDEX);
+ });
+ }
+ }
+
+ public static function drop(Builder $schema): void
+ {
+ $schema->dropIfExists(self::CHANGES);
+ $schema->dropIfExists(self::FLAGS);
+ }
+}
diff --git a/packages/feature-flags/src/Store/FeatureFlagStoreConfiguration.php b/packages/feature-flags/src/Store/FeatureFlagStoreConfiguration.php
new file mode 100644
index 00000000..f0da5a37
--- /dev/null
+++ b/packages/feature-flags/src/Store/FeatureFlagStoreConfiguration.php
@@ -0,0 +1,56 @@
+store->driver === 'memory') {
+ return new MemoryFlagStore;
+ }
+
+ // Resolve only for the database driver: a nullable resolver parameter is still made by Laravel.
+ $databases = $app->bound('db') ? $app->make('db') : null;
+ if (! $databases instanceof ConnectionResolverInterface) {
+ throw new ConfigurationException('firefly.feature-flags.sources.store.driver is database but no database connection resolver (db) is bound.');
+ }
+
+ return new DatabaseFlagStore($databases->connection($settings->store->connection));
+ }
+
+ #[Bean]
+ #[ConditionalOnProperty(name: 'firefly.feature-flags.enabled', havingValue: 'true')]
+ #[ConditionalOnProperty(name: 'firefly.feature-flags.sources.store.enabled', havingValue: 'true')]
+ #[ConditionalOnBean(FlagRegistry::class)]
+ #[ConditionalOnMissingBean(FlagStoreWriter::class)]
+ public function flagStoreWriter(FlagStore $store, FlagRegistry $registry, ApplicationEventPublisher $events, ?LoggerInterface $logger = null): FlagStoreWriter
+ {
+ return new FlagStoreWriter($store, $registry, $events, $logger ?? new NullLogger);
+ }
+}
diff --git a/packages/feature-flags/src/Store/FlagChange.php b/packages/feature-flags/src/Store/FlagChange.php
new file mode 100644
index 00000000..b4471978
--- /dev/null
+++ b/packages/feature-flags/src/Store/FlagChange.php
@@ -0,0 +1,40 @@
+|null $definition
+ * @param array|null $previous
+ */
+ public function __construct(
+ public int $id,
+ public string $key,
+ public string $action,
+ public ?array $definition,
+ public ?array $previous,
+ public ?string $actor,
+ public DateTimeImmutable $changedAt,
+ ) {}
+
+ /** @return array{id: int, action: string, actor: ?string, changedAt: string} */
+ public function toHistoryRow(): array
+ {
+ return [
+ 'id' => $this->id,
+ 'action' => $this->action,
+ 'actor' => $this->actor,
+ 'changedAt' => $this->changedAt->setTimezone(new DateTimeZone('UTC'))->format('Y-m-d\TH:i:s\Z'),
+ ];
+ }
+}
diff --git a/packages/feature-flags/src/Store/FlagStore.php b/packages/feature-flags/src/Store/FlagStore.php
new file mode 100644
index 00000000..2a158c26
--- /dev/null
+++ b/packages/feature-flags/src/Store/FlagStore.php
@@ -0,0 +1,23 @@
+ */
+ public function all(): array;
+
+ public function get(string $key): ?StoredFlag;
+
+ public function revision(): int;
+
+ /** @param array $definition */
+ public function put(string $key, array $definition, ?string $actor, ?int $expectedVersion = null): FlagChange;
+
+ public function delete(string $key, ?string $actor, ?int $expectedVersion = null): ?FlagChange;
+
+ /** @return list */
+ public function history(string $key, int $limit = 50): array;
+}
diff --git a/packages/feature-flags/src/Store/FlagStoreConflict.php b/packages/feature-flags/src/Store/FlagStoreConflict.php
new file mode 100644
index 00000000..44c6d65c
--- /dev/null
+++ b/packages/feature-flags/src/Store/FlagStoreConflict.php
@@ -0,0 +1,27 @@
+store;
+ }
+
+ /** @throws InvalidFlagDefinition|FlagStoreConflict */
+ public function put(string $key, mixed $definition, ?string $actor, ?int $expectedVersion = null): FlagChange
+ {
+ // Parsing validates once and preserves object-shaped values, including an empty object variant.
+ $document = FlagDefinitions::parseDocument(['flags' => Json::object([$key => $definition])]);
+ $flag = $document->flag($key);
+ if ($flag === null) {
+ throw new InvalidFlagDefinition($key, 'flag definition must be an object');
+ }
+ $validated = Json::members(Json::decode(Json::canonical($flag->toJsonValue())));
+ $change = $this->store->put($key, $validated, $actor, $expectedVersion);
+ $this->afterCommit($change);
+
+ return $change;
+ }
+
+ /** @throws FlagStoreConflict */
+ public function delete(string $key, ?string $actor, ?int $expectedVersion = null): ?FlagChange
+ {
+ $change = $this->store->delete($key, $actor, $expectedVersion);
+ if ($change !== null) {
+ $this->afterCommit($change);
+ }
+
+ return $change;
+ }
+
+ private function afterCommit(FlagChange $change): void
+ {
+ // Snapshot now: a SQL callback may run much later, after the caller mutates its input or another write.
+ $previous = $change->previous === null ? null : Json::encode(Json::object($change->previous));
+ $current = $change->definition === null ? null : Json::encode(Json::object($change->definition));
+ $notify = function () use ($change, $previous, $current): void {
+ try {
+ $this->registry->refresh(force: true, only: FlagSource::STORE);
+ foreach ($this->registry->states() as $state) {
+ if ($state->name === FlagSource::STORE && $state->status() !== 'UP') {
+ $this->warn('Feature flag store refresh failed after commit ({error}).', $change, new RuntimeException($state->error ?? $state->status()));
+ break;
+ }
+ }
+ } catch (Throwable $error) {
+ $this->warn('Feature flag store refresh failed after commit ({error}).', $change, $error);
+ }
+
+ try {
+ $this->events->publish(new FeatureFlagUpdated(
+ $change->key,
+ $change->action,
+ $change->actor,
+ $previous === null ? null : Json::members(Json::decode($previous)),
+ $current === null ? null : Json::members(Json::decode($current)),
+ ));
+ } catch (Throwable $error) {
+ $this->warn('FeatureFlagUpdated listener failed after commit ({error}).', $change, $error);
+ }
+ };
+
+ if ($this->store instanceof CommitAwareFlagStore) {
+ $this->store->afterCommit($notify);
+ } else {
+ $notify();
+ }
+ }
+
+ private function warn(string $message, FlagChange $change, Throwable $error): void
+ {
+ try {
+ $this->logger->warning($message, ['key' => $change->key, 'action' => $change->action, 'error' => $error->getMessage()]);
+ } catch (Throwable) {
+ // Logging is an observer too; the committed change must still return.
+ }
+ }
+}
diff --git a/packages/feature-flags/src/Store/MemoryFlagStore.php b/packages/feature-flags/src/Store/MemoryFlagStore.php
new file mode 100644
index 00000000..ecd1768b
--- /dev/null
+++ b/packages/feature-flags/src/Store/MemoryFlagStore.php
@@ -0,0 +1,112 @@
+ */
+ private array $flags = [];
+
+ /** @var list */
+ private array $changes = [];
+
+ private readonly Closure $clock;
+
+ /** @param (Closure(): DateTimeImmutable)|null $clock */
+ public function __construct(?Closure $clock = null)
+ {
+ $this->clock = $clock ?? static fn (): DateTimeImmutable => new DateTimeImmutable('now', new DateTimeZone('UTC'));
+ }
+
+ public function all(): array
+ {
+ $flags = [];
+ foreach ($this->flags as $key => $flag) {
+ $flags[$key] = $this->copyFlag($flag);
+ }
+ ksort($flags, SORT_STRING);
+
+ return $flags;
+ }
+
+ public function get(string $key): ?StoredFlag
+ {
+ $flag = $this->flags[$key] ?? null;
+
+ return $flag === null ? null : $this->copyFlag($flag);
+ }
+
+ public function revision(): int
+ {
+ return count($this->changes);
+ }
+
+ public function put(string $key, array $definition, ?string $actor, ?int $expectedVersion = null): FlagChange
+ {
+ $current = $this->flags[$key] ?? null;
+ FlagStoreConflict::check($key, $expectedVersion, $current?->version);
+ $now = ($this->clock)();
+ $owned = self::copyDefinition($definition);
+ $next = new StoredFlag($key, $owned, ($current === null ? 0 : $current->version) + 1, $now, $actor);
+ $change = new FlagChange(count($this->changes) + 1, $key, FlagChange::PUT, $owned, $current?->definition, $actor, $now);
+ $this->flags[$key] = $next;
+ $this->changes[] = $change;
+
+ return $this->copyChange($change);
+ }
+
+ public function delete(string $key, ?string $actor, ?int $expectedVersion = null): ?FlagChange
+ {
+ $current = $this->flags[$key] ?? null;
+ FlagStoreConflict::check($key, $expectedVersion, $current?->version);
+ if ($current === null) {
+ return null;
+ }
+ $now = ($this->clock)();
+ $change = new FlagChange(count($this->changes) + 1, $key, FlagChange::DELETE, null, $current->definition, $actor, $now);
+ unset($this->flags[$key]);
+ $this->changes[] = $change;
+
+ return $this->copyChange($change);
+ }
+
+ public function history(string $key, int $limit = 50): array
+ {
+ $rows = array_values(array_filter($this->changes, static fn (FlagChange $change): bool => $change->key === $key));
+
+ return array_map($this->copyChange(...), array_slice(array_reverse($rows), 0, max(0, $limit)));
+ }
+
+ /** @param array $definition
+ * @return array
+ */
+ private static function copyDefinition(array $definition): array
+ {
+ return Json::members(Json::decode(Json::canonical(Json::object($definition))));
+ }
+
+ private function copyFlag(StoredFlag $flag): StoredFlag
+ {
+ return new StoredFlag($flag->key, self::copyDefinition($flag->definition), $flag->version, $flag->updatedAt, $flag->updatedBy);
+ }
+
+ private function copyChange(FlagChange $change): FlagChange
+ {
+ return new FlagChange(
+ $change->id,
+ $change->key,
+ $change->action,
+ $change->definition === null ? null : self::copyDefinition($change->definition),
+ $change->previous === null ? null : self::copyDefinition($change->previous),
+ $change->actor,
+ $change->changedAt,
+ );
+ }
+}
diff --git a/packages/feature-flags/src/Store/StoreCommitCallbacks.php b/packages/feature-flags/src/Store/StoreCommitCallbacks.php
new file mode 100644
index 00000000..e7afd60f
--- /dev/null
+++ b/packages/feature-flags/src/Store/StoreCommitCallbacks.php
@@ -0,0 +1,99 @@
+ */
+ private array $pending = [];
+
+ private ?Throwable $nativeAbort = null;
+
+ public function __construct(private readonly Connection $connection)
+ {
+ $dispatcher = $connection->getEventDispatcher();
+ if ($dispatcher === null) {
+ $dispatcher = new Dispatcher;
+ $connection->setEventDispatcher($dispatcher);
+ }
+ $dispatcher->listen(TransactionBeginning::class, function (TransactionBeginning $event): void {
+ if ($event->connection === $this->connection && $event->connection->transactionLevel() === 1) {
+ $this->pending = [];
+ $this->nativeAbort = null;
+ }
+ });
+ $dispatcher->listen(TransactionCommitting::class, function (TransactionCommitting $event): void {
+ if ($event->connection === $this->connection && $this->nativeAbort !== null) {
+ $cause = $this->nativeAbort;
+ // transaction(callback) decrements its level after a commit exception. End PDO's stale
+ // transaction first, while Laravel can still perform a root rollback; never report commit success.
+ try {
+ $this->connection->rollBack(0);
+ } finally {
+ throw new FlagStoreTransactionAborted($cause);
+ }
+ }
+ });
+ $dispatcher->listen(TransactionCommitted::class, function (TransactionCommitted $event): void {
+ if ($event->connection !== $this->connection || $this->nativeAbort !== null) {
+ return;
+ }
+ $level = $event->connection->transactionLevel();
+ if ($level > 0) {
+ $this->pending = array_map(static fn (array $entry): array => [min($entry[0], $level), $entry[1]], $this->pending);
+
+ return;
+ }
+ $callbacks = $this->pending;
+ $this->pending = [];
+ foreach ($callbacks as [, $callback]) {
+ $callback();
+ }
+ });
+ $dispatcher->listen(TransactionRolledBack::class, function (TransactionRolledBack $event): void {
+ if ($event->connection === $this->connection) {
+ $level = $event->connection->transactionLevel();
+ $this->pending = array_values(array_filter($this->pending, static fn (array $entry): bool => $entry[0] <= $level));
+ }
+ });
+ }
+
+ public function nativeTransactionAborted(Throwable $cause): void
+ {
+ $this->nativeAbort = $cause;
+ $this->pending = [];
+ }
+
+ /** @param Closure(): void $callback */
+ public function afterCommit(Closure $callback): void
+ {
+ if ($this->nativeAbort !== null) {
+ return;
+ }
+ $level = $this->connection->transactionLevel();
+ if ($level === 0) {
+ $callback();
+ } else {
+ $this->pending[] = [$level, $callback];
+ }
+ }
+}
diff --git a/packages/feature-flags/src/Store/StoredFlag.php b/packages/feature-flags/src/Store/StoredFlag.php
new file mode 100644
index 00000000..9906268b
--- /dev/null
+++ b/packages/feature-flags/src/Store/StoredFlag.php
@@ -0,0 +1,19 @@
+ $definition */
+ public function __construct(
+ public string $key,
+ public array $definition,
+ public int $version,
+ public DateTimeImmutable $updatedAt,
+ public ?string $updatedBy,
+ ) {}
+}
diff --git a/packages/feature-flags/src/Store/TransactionAwareFlagStore.php b/packages/feature-flags/src/Store/TransactionAwareFlagStore.php
new file mode 100644
index 00000000..123ee131
--- /dev/null
+++ b/packages/feature-flags/src/Store/TransactionAwareFlagStore.php
@@ -0,0 +1,11 @@
+ the exposure noted for each evaluation in flight */
+ private WeakMap $exposures;
+
+ public function __construct(
+ private readonly ApplicationEventPublisher $events,
+ private readonly LoggerInterface $logger = new NullLogger,
+ ) {
+ $this->exposures = new WeakMap;
+ }
+
+ public function before(HookContext $context, HookHints $hints): ?EvaluationContext
+ {
+ return null;
+ }
+
+ public function after(HookContext $context, ResolutionDetails $details, HookHints $hints): void
+ {
+ if ($hints->get(FeatureFlags::PREVIEW_HINT) === true) {
+ return;
+ }
+
+ unset($this->exposures[$context]);
+ try {
+ $error = $details->getError();
+ $failed = $error !== null || $details->getReason() === 'ERROR';
+
+ $this->exposures[$context] = new FeatureFlagEvaluated(
+ $context->getFlagKey(),
+ ValueCopy::forExposure($details->getValue()),
+ $failed ? null : $details->getVariant(),
+ $failed ? 'ERROR' : ($details->getReason() ?? 'UNKNOWN'),
+ $error?->getResolutionErrorCode()->getValue(),
+ $context->getEvaluationContext()->getTargetingKey(),
+ );
+ } catch (Throwable $failure) {
+ $this->logFailure($context->getFlagKey(), $failure);
+ }
+ }
+
+ public function error(HookContext $context, Throwable $error, HookHints $hints): void
+ {
+ if ($hints->get(FeatureFlags::PREVIEW_HINT) === true) {
+ return;
+ }
+
+ unset($this->exposures[$context]);
+ try {
+ $this->exposures[$context] = new FeatureFlagEvaluated(
+ $context->getFlagKey(),
+ ValueCopy::forExposure($context->getDefaultValue()),
+ null,
+ 'ERROR',
+ self::errorCode($error),
+ $context->getEvaluationContext()->getTargetingKey(),
+ );
+ } catch (Throwable $failure) {
+ $this->logFailure($context->getFlagKey(), $failure);
+ }
+ }
+
+ public function finally(HookContext $context, HookHints $hints): void
+ {
+ $event = $this->exposures[$context] ?? null;
+ unset($this->exposures[$context]);
+ if ($event === null) {
+ return;
+ }
+
+ try {
+ $this->events->publish($event);
+ } catch (Throwable $failure) {
+ $this->logFailure($event->key, $failure);
+ }
+ }
+
+ public function supportsFlagValueType(string $flagValueType): bool
+ {
+ return true;
+ }
+
+ private function logFailure(string $key, Throwable $failure): void
+ {
+ try {
+ $this->logger->debug('Publishing the exposure of feature flag [{flag}] failed: {error}', ['flag' => $key, 'error' => $failure->getMessage()]);
+ } catch (Throwable) {
+ // Logging is best effort too: telemetry must not change an evaluation.
+ }
+ }
+
+ /** The code the SDK answers a thrown failure with: the one it carries, else GENERAL. */
+ private static function errorCode(Throwable $error): string
+ {
+ $general = ErrorCode::GENERAL()->getValue();
+ if (! $error instanceof ThrowableWithResolutionError) {
+ return $general;
+ }
+
+ try {
+ return $error->getResolutionError()->getResolutionErrorCode()->getValue();
+ } catch (Throwable) {
+ return $general;
+ }
+ }
+}
diff --git a/packages/feature-flags/src/Telemetry/FeatureFlagMetrics.php b/packages/feature-flags/src/Telemetry/FeatureFlagMetrics.php
new file mode 100644
index 00000000..e797722e
--- /dev/null
+++ b/packages/feature-flags/src/Telemetry/FeatureFlagMetrics.php
@@ -0,0 +1,14 @@
+ the [variant, reason] noted for each evaluation in flight */
+ private WeakMap $outcomes;
+
+ public function __construct(
+ private readonly FeatureFlagMetrics $metrics,
+ private readonly LoggerInterface $logger = new NullLogger,
+ ) {
+ $this->outcomes = new WeakMap;
+ }
+
+ public function before(HookContext $context, HookHints $hints): ?EvaluationContext
+ {
+ return null;
+ }
+
+ public function after(HookContext $context, ResolutionDetails $details, HookHints $hints): void
+ {
+ if ($hints->get(FeatureFlags::PREVIEW_HINT) === true) {
+ return;
+ }
+
+ $variant = $details->getVariant();
+ $this->outcomes[$context] = $details->getError() !== null || $details->getReason() === 'ERROR'
+ ? ['none', 'ERROR']
+ : [$variant === null || $variant === '' ? 'none' : $variant, $details->getReason() ?? 'UNKNOWN'];
+ }
+
+ public function error(HookContext $context, Throwable $error, HookHints $hints): void
+ {
+ if ($hints->get(FeatureFlags::PREVIEW_HINT) === true) {
+ return;
+ }
+
+ $this->outcomes[$context] = ['none', 'ERROR'];
+ }
+
+ public function finally(HookContext $context, HookHints $hints): void
+ {
+ $outcome = $this->outcomes[$context] ?? null;
+ unset($this->outcomes[$context]);
+ if ($outcome === null) {
+ return;
+ }
+
+ [$variant, $reason] = $outcome;
+ try {
+ $this->metrics->recordEvaluation($context->getFlagKey(), $variant, $reason);
+ } catch (Throwable $failure) {
+ try {
+ $this->logger->debug('Recording the evaluation of feature flag [{flag}] failed: {error}', ['flag' => $context->getFlagKey(), 'error' => $failure->getMessage()]);
+ } catch (Throwable) {
+ // Logging is best effort too: telemetry must not change an evaluation.
+ }
+ }
+ }
+
+ public function supportsFlagValueType(string $flagValueType): bool
+ {
+ return true;
+ }
+}
diff --git a/packages/feature-flags/src/Telemetry/NoOpFeatureFlagMetrics.php b/packages/feature-flags/src/Telemetry/NoOpFeatureFlagMetrics.php
new file mode 100644
index 00000000..351d6889
--- /dev/null
+++ b/packages/feature-flags/src/Telemetry/NoOpFeatureFlagMetrics.php
@@ -0,0 +1,10 @@
+ dirname(__DIR__).'/src'];
+ $cache = dirname(__DIR__).'/cache';
+
+ $components = sys_get_temp_dir().'/firefly-feature-flags-fresh-c-'.bin2hex(random_bytes(6)).'.php';
+ $context = sys_get_temp_dir().'/firefly-feature-flags-fresh-x-'.bin2hex(random_bytes(6)).'.php';
+
+ try {
+ (new AutoConfigManifestCompiler)->write($src, $components, $context);
+
+ expect(ComponentManifest::load($components))->toEqual(ComponentManifest::load($cache.'/firefly-feature-flags-components.php'))
+ ->and(ContextManifest::load($context))->toEqual(ContextManifest::load($cache.'/firefly-feature-flags-context.php'));
+ } finally {
+ @unlink($components);
+ @unlink($context);
+ }
+});
diff --git a/packages/feature-flags/tests/Conformance/MANIFEST.sha256 b/packages/feature-flags/tests/Conformance/MANIFEST.sha256
new file mode 100644
index 00000000..303e4e9d
--- /dev/null
+++ b/packages/feature-flags/tests/Conformance/MANIFEST.sha256
@@ -0,0 +1,24 @@
+f47ea73118b7efa7648b944b9921e5120a711db35c26d88e15a12958c54d8097 cli-input-vectors.json
+f7b17c03618b3c03b387c3c6ea49ce2b46cff3429f0a1aebc86dc7788f388b5a context-vectors.json
+d14f713aa8196d765f3bd9d886b52a73e24ec0acddbe5c4a0dfeba926cfa01a1 exposure-vectors.json
+2065a5672a54a72adc125932ece631824d04a7524cc73e15be82612ad5cb9033 firefly-vectors.json
+8f656618d96a13ff5bb93ba30189bd3e328970a3915bfd39f6f9bb249e7419fe http-vectors.json
+a41894e067cd117e3a146da1ac1e0c0522ca50dc28b8aa80ba83003c2246e332 management-transport-vectors.json
+be87cb28bd6075ab4b03069a2f2f88531b349eee99f92d5f52215e98dd9027f8 management-vectors.json
+46b02fa95a14af3e3598ea255c3771f7914765a6d3988402ad10f0a184131bdf observer-vectors.json
+bf92b09408fb31563c98e15741ab98d68ca1d511763cdc1cb5a2f0ddd23d54ac override-vectors.json
+0b2f730da0fa8298e1024ac4461c49ac8e5b601a6c98a5e69d96b66fba706601 store-concurrency-vectors.json
+f964c4c458fcc3741ac1639dc28bef1e42c016b37cab9fe84400fc3b597dc5ab testbed/LICENSE
+b1dbd97a0907abffb384923f5a140d1307b2c4bb2b3299fc3e3c11406b941334 testbed/SOURCE.md
+62feadf4fbf95696e56839f8e0feafc36d9573f67a37c22a96722e5721bbb2d4 testbed/evaluator/flags/testkit-flags.json
+5b0cd778e21fed444feedbfa5a25ac058a30809ce4b44d6df56c5a2840e1e599 testbed/evaluator/gherkin/disabled.feature
+a6cfaab4518dcfb3ee5b0f0886f84c3b1a1f2948f4ae289599ea731cd0139240 testbed/evaluator/gherkin/errors.feature
+407d12c02a2c0213258b6e61cf914eb80f18a8d57318398f85e6cf5d4c07cf03 testbed/evaluator/gherkin/evaluation.feature
+1f3c6ce05e4bb3f96f202ce2a79bcf72d5575775b1010f746d63eb66d26fc33b testbed/evaluator/gherkin/evaluator-refs.feature
+95adac13ab953cf78e1fff919ce66b2395f716cd988537a7119ec66bbeb7a82b testbed/evaluator/gherkin/fractional.feature
+eec9ba093968ce4ed6cc2b68ac70c7f197545fb310c335efeba76a893fa13122 testbed/evaluator/gherkin/metadata.feature
+78a6255f0dc1846cd5a148fc0f7f4b20109c50590a94b7a76b73c67bfe8f9c5e testbed/evaluator/gherkin/no-default-variant.feature
+4bba88f15e3e8266af4c19bfa86ece882c19e2b406ac21b9543128ccc616c57f testbed/evaluator/gherkin/semver.feature
+2ed75d12b2e4710370645f60923f540b014a80653c43bf857a1f43888dcf673d testbed/evaluator/gherkin/string.feature
+9e381b848f62336e725bd283d5e1581c1526fe67dc59487f14161757a448aa01 testbed/evaluator/gherkin/targeting.feature
+e0e9239ada2e2d5eb534b1c8bd35adc690ba58fdad3dafd39d03eecccd071e84 testbed/evaluator/gherkin/zero-values.feature
diff --git a/packages/feature-flags/tests/Conformance/cli-input-vectors.json b/packages/feature-flags/tests/Conformance/cli-input-vectors.json
new file mode 100644
index 00000000..9f73644d
--- /dev/null
+++ b/packages/feature-flags/tests/Conformance/cli-input-vectors.json
@@ -0,0 +1 @@
+{"version":1,"flags":{"kill":true},"action":"evaluate","key":"kill","cases":[{"name":"omitted context","options":{},"expect":{"exit":0,"value":true}},{"name":"empty context text","options":{"context":""},"expect":{"exit":1,"error":"bad-request"}},{"name":"whitespace context","options":{"context":" "},"expect":{"exit":1,"error":"bad-request"}},{"name":"malformed context","options":{"context":"{bad"},"expect":{"exit":1,"error":"bad-request"}},{"name":"null context","options":{"context":"null"},"expect":{"exit":1,"error":"bad-request"}},{"name":"empty list context","options":{"context":"[]"},"expect":{"exit":1,"error":"bad-request"}},{"name":"list context","options":{"context":"[1]"},"expect":{"exit":1,"error":"bad-request"}},{"name":"boolean context","options":{"context":"true"},"expect":{"exit":1,"error":"bad-request"}},{"name":"number context","options":{"context":"1"},"expect":{"exit":1,"error":"bad-request"}},{"name":"string context","options":{"context":"\"text\""},"expect":{"exit":1,"error":"bad-request"}},{"name":"empty object context","options":{"context":"{}"},"expect":{"exit":0,"value":true}},{"name":"object context","options":{"context":"{\"plan\":\"pro\"}"},"expect":{"exit":0,"value":true}}]}
diff --git a/packages/feature-flags/tests/Conformance/context-vectors.json b/packages/feature-flags/tests/Conformance/context-vectors.json
new file mode 100644
index 00000000..3a615916
--- /dev/null
+++ b/packages/feature-flags/tests/Conformance/context-vectors.json
@@ -0,0 +1 @@
+{"version":1,"cases":[{"name":"integer","ambientKey":"ambient","context":{"targetingKey":42,"plan":"pro"},"expect":{"targetingKey":"42","attributes":{"plan":"pro"},"refused":false}},{"name":"text","ambientKey":"ambient","context":{"targetingKey":"42","plan":"pro"},"expect":{"targetingKey":"42","attributes":{"plan":"pro"},"refused":false}},{"name":"zero integer","ambientKey":"ambient","context":{"targetingKey":0,"plan":"pro"},"expect":{"targetingKey":"0","attributes":{"plan":"pro"},"refused":false}},{"name":"negative integer","ambientKey":"ambient","context":{"targetingKey":-42,"plan":"pro"},"expect":{"targetingKey":"-42","attributes":{"plan":"pro"},"refused":false}},{"name":"boolean true","ambientKey":"ambient","context":{"targetingKey":true,"plan":"pro"},"expect":{"targetingKey":"ambient","attributes":{"plan":"pro"},"refused":true}},{"name":"boolean false","ambientKey":"ambient","context":{"targetingKey":false,"plan":"pro"},"expect":{"targetingKey":"ambient","attributes":{"plan":"pro"},"refused":true}},{"name":"whole float","ambientKey":"ambient","context":{"targetingKey":42.0,"plan":"pro"},"expect":{"targetingKey":"ambient","attributes":{"plan":"pro"},"refused":true}},{"name":"fractional float","ambientKey":"ambient","context":{"targetingKey":42.5,"plan":"pro"},"expect":{"targetingKey":"ambient","attributes":{"plan":"pro"},"refused":true}},{"name":"object","ambientKey":"ambient","context":{"targetingKey":{},"plan":"pro"},"expect":{"targetingKey":"ambient","attributes":{"plan":"pro"},"refused":true}},{"name":"list","ambientKey":"ambient","context":{"targetingKey":[],"plan":"pro"},"expect":{"targetingKey":"ambient","attributes":{"plan":"pro"},"refused":true}},{"name":"null","ambientKey":"ambient","context":{"targetingKey":null,"plan":"pro"},"expect":{"targetingKey":"ambient","attributes":{"plan":"pro"},"refused":false}},{"name":"empty text","ambientKey":"ambient","context":{"targetingKey":"","plan":"pro"},"expect":{"targetingKey":"ambient","attributes":{"plan":"pro"},"refused":false}}]}
diff --git a/packages/feature-flags/tests/Conformance/exposure-vectors.json b/packages/feature-flags/tests/Conformance/exposure-vectors.json
new file mode 100644
index 00000000..599220dd
--- /dev/null
+++ b/packages/feature-flags/tests/Conformance/exposure-vectors.json
@@ -0,0 +1 @@
+{"version":1,"limit":10000,"cases":[{"name":"scalar snapshot","value":true,"occurrences":1,"expose":true},{"name":"nested containers and null","value":{"items":[null,false,1,"x"],"empty":{}},"occurrences":7,"expose":true},{"name":"list at 10000","value":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"occurrences":10000,"expose":true},{"name":"list over 10000","value":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"occurrences":10001,"expose":false},{"name":"object at 10000","value":{"items":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},"occurrences":10000,"expose":true},{"name":"object over 10000","value":{"items":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},"occurrences":10001,"expose":false}]}
diff --git a/packages/feature-flags/tests/Conformance/firefly-vectors.json b/packages/feature-flags/tests/Conformance/firefly-vectors.json
new file mode 100644
index 00000000..758a157d
--- /dev/null
+++ b/packages/feature-flags/tests/Conformance/firefly-vectors.json
@@ -0,0 +1,104 @@
+{
+ "version": 1,
+ "today": "2026-10-01",
+ "cases": [
+ {"name": "shorthand true is an enabled boolean flag on 'on'", "kind": "normalize", "input": {"kill-switch": true}, "expect": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}}},
+ {"name": "shorthand false is an enabled boolean flag on 'off'", "kind": "normalize", "input": {"new-checkout": false}, "expect": {"new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off"}}},
+ {"name": "shorthand string is a single-variant string flag named by its value", "kind": "normalize", "input": {"checkout-flow": "v2"}, "expect": {"checkout-flow": {"state": "ENABLED", "variants": {"v2": "v2"}, "defaultVariant": "v2"}}},
+ {"name": "a full definition passes through unchanged", "kind": "normalize", "input": {"banner-color": {"state": "DISABLED", "variants": {"red": "#f00", "blue": "#00f"}, "defaultVariant": "blue", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "red", null]}, "metadata": {"owner": "web", "kind": "experiment", "expires": "2026-12-31", "rank": 3, "sticky": true}}}, "expect": {"banner-color": {"state": "DISABLED", "variants": {"red": "#f00", "blue": "#00f"}, "defaultVariant": "blue", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "red", null]}, "metadata": {"owner": "web", "kind": "experiment", "expires": "2026-12-31", "rank": 3, "sticky": true}}}},
+ {"name": "shorthand and full definitions mix in one map", "kind": "normalize", "input": {"a": true, "b": "blue", "c": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off"}}, "expect": {"a": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "b": {"state": "ENABLED", "variants": {"blue": "blue"}, "defaultVariant": "blue"}, "c": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off"}}},
+ {"name": "a well-formed document is valid", "kind": "validate", "input": {"flags": {"a.b_c-1": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off"}, "no-default": {"state": "ENABLED", "variants": {"x": 1, "y": 2}}, "null-default": {"state": "ENABLED", "variants": {"x": 1.5}, "defaultVariant": null}, "empty-targeting": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {}}, "objects": {"state": "ENABLED", "variants": {"a": {"k": 1}, "b": [1, 2]}, "defaultVariant": "a"}, "with-ref": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta"}, "on", null]}}, "with-missing-ref": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "nope"}, "on", null]}}, "meta": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "metadata": {"description": "d", "owner": "o", "kind": "ops", "expires": "2027-01-31", "n": 1, "f": 0.5, "b": false}}}, "$evaluators": {"beta": {"in": ["beta", {"var": "roles"}]}}}, "expect": {"valid": true}},
+ {"name": "a key with a space is invalid", "kind": "validate", "input": {"flags": {"bad key": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off"}}}, "expect": {"valid": false, "key": "bad key", "error": "invalid flag key"}},
+ {"name": "a key starting with a dash is invalid", "kind": "validate", "input": {"flags": {"-lead": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off"}}}, "expect": {"valid": false, "key": "-lead", "error": "invalid flag key"}},
+ {"name": "a key longer than 128 characters is invalid", "kind": "validate", "input": {"flags": {"kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off"}}}, "expect": {"valid": false, "key": "kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk", "error": "invalid flag key"}},
+ {"name": "state must be ENABLED or DISABLED", "kind": "validate", "input": {"flags": {"s": {"state": "ON", "variants": {"on": true, "off": false}, "defaultVariant": "off"}}}, "expect": {"valid": false, "key": "s", "error": "state must be ENABLED or DISABLED"}},
+ {"name": "a missing state is invalid", "kind": "validate", "input": {"flags": {"s": {"variants": {"on": true}, "defaultVariant": "on"}}}, "expect": {"valid": false, "key": "s", "error": "state must be ENABLED or DISABLED"}},
+ {"name": "variants must be a non-empty object", "kind": "validate", "input": {"flags": {"v": {"state": "ENABLED", "variants": {}}}}, "expect": {"valid": false, "key": "v", "error": "variants must be a non-empty object"}},
+ {"name": "variants as a list are invalid", "kind": "validate", "input": {"flags": {"v": {"state": "ENABLED", "variants": [true, false]}}}, "expect": {"valid": false, "key": "v", "error": "variants must be a non-empty object"}},
+ {"name": "variants must share one type", "kind": "validate", "input": {"flags": {"m": {"state": "ENABLED", "variants": {"a": true, "b": "x"}}}}, "expect": {"valid": false, "key": "m", "error": "variants must share one type"}},
+ {"name": "integer and float variants share the number type only if all are numbers", "kind": "validate", "input": {"flags": {"n": {"state": "ENABLED", "variants": {"a": 1, "b": "2"}}}}, "expect": {"valid": false, "key": "n", "error": "variants must share one type"}},
+ {"name": "defaultVariant must name a variant", "kind": "validate", "input": {"flags": {"d": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "maybe"}}}, "expect": {"valid": false, "key": "d", "error": "defaultVariant is not a variant"}},
+ {"name": "targeting must be an object", "kind": "validate", "input": {"flags": {"t": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": ["on"]}}}, "expect": {"valid": false, "key": "t", "error": "targeting must be an object"}},
+ {"name": "metadata values must be scalars", "kind": "validate", "input": {"flags": {"md": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "metadata": {"tags": ["a"]}}}}, "expect": {"valid": false, "key": "md", "error": "metadata values must be scalars"}},
+ {"name": "kind must be a known kind", "kind": "validate", "input": {"flags": {"k": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "metadata": {"kind": "temporary"}}}}, "expect": {"valid": false, "key": "k", "error": "kind must be one of release, experiment, ops, permission"}},
+ {"name": "expires must be a calendar date", "kind": "validate", "input": {"flags": {"e": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "metadata": {"expires": "2026-13-01"}}}}, "expect": {"valid": false, "key": "e", "error": "expires must be a YYYY-MM-DD date"}},
+ {"name": "expires must not carry a time", "kind": "validate", "input": {"flags": {"e2": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "metadata": {"expires": "2026-12-01T00:00:00Z"}}}}, "expect": {"valid": false, "key": "e2", "error": "expires must be a YYYY-MM-DD date"}},
+ {"name": "owner must be a string", "kind": "validate", "input": {"flags": {"o": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "metadata": {"owner": 7}}}}, "expect": {"valid": false, "key": "o", "error": "owner must be a string"}},
+ {"name": "description must be a string", "kind": "validate", "input": {"flags": {"ds": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "metadata": {"description": true}}}}, "expect": {"valid": false, "key": "ds", "error": "description must be a string"}},
+ {"name": "a flag definition must be an object", "kind": "validate", "input": {"flags": {"x": 42}}, "expect": {"valid": false, "key": "x", "error": "flag definition must be an object"}},
+ {"name": "flag metadata keys must not be empty", "kind": "validate", "input": {"flags": {"mk": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "metadata": {"": "x"}}}}, "expect": {"valid": false, "key": "mk", "error": "metadata keys must not be empty"}},
+ {"name": "a null variant value is not a flag value", "kind": "validate", "input": {"flags": {"nv": {"state": "ENABLED", "variants": {"a": null}}}}, "expect": {"valid": false, "key": "nv", "error": "variants must share one type"}},
+ {"name": "a null variant value cannot share a type with others", "kind": "validate", "input": {"flags": {"nv2": {"state": "ENABLED", "variants": {"a": "x", "b": null}, "defaultVariant": "a"}}}, "expect": {"valid": false, "key": "nv2", "error": "variants must share one type"}},
+ {"name": "an evaluator rule must be an object", "kind": "validate", "input": {"flags": {"x": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off"}}, "$evaluators": {"broken": 5}}, "expect": {"valid": false, "key": "$evaluators", "error": "targeting must be an object"}},
+ {"name": "a definition nesting deeper than 256 levels is invalid", "kind": "validate", "input": {"flags": {"deep": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [true]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}}}}, "expect": {"valid": false, "key": "deep", "error": "definition nests too deeply"}},
+ {"name": "a definition nesting about 200 levels is valid", "kind": "validate", "input": {"flags": {"deep-ok": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [{"and": [true]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}}}}, "expect": {"valid": true}},
+ {"name": "an empty defaultVariant naming a variant called \"\" is valid", "kind": "validate", "input": {"flags": {"ed": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}}}, "expect": {"valid": true}},
+ {"name": "an empty list stands for an empty object in targeting, metadata and the document sections", "kind": "validate", "input": {"flags": {"el": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": [], "metadata": []}}, "$evaluators": []}, "expect": {"valid": true}},
+ {"name": "the highest layer defining a key supplies its whole definition", "kind": "compose", "layers": [{"source": "config", "flags": {"alpha": true, "checkout-flow": "v1", "gamma": false}}, {"source": "file", "flags": {"checkout-flow": {"state": "ENABLED", "variants": {"v1": "v1", "v2": "v2"}, "defaultVariant": "v1"}}}, {"source": "http", "flags": {"gamma": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on", "metadata": {"owner": "ops"}}}}, {"source": "store", "flags": {"checkout-flow": {"state": "DISABLED", "variants": {"v1": "v1", "v2": "v2"}, "defaultVariant": "v2"}}}], "expect": {"flags": {"alpha": {"origin": "config", "overrides": [], "definition": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}}, "checkout-flow": {"origin": "store", "overrides": ["config", "file"], "definition": {"state": "DISABLED", "variants": {"v1": "v1", "v2": "v2"}, "defaultVariant": "v2"}}, "gamma": {"origin": "http", "overrides": ["config"], "definition": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on", "metadata": {"owner": "ops"}}}}}},
+ {"name": "a disabled definition in a higher layer still shadows the lower one", "kind": "compose", "layers": [{"source": "config", "flags": {"beta": true}}, {"source": "store", "flags": {"beta": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}}}], "expect": {"flags": {"beta": {"origin": "store", "overrides": ["config"], "definition": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}}}}},
+ {"name": "evaluators merge per name with the same precedence", "kind": "compose", "layers": [{"source": "config", "flags": {"x": true}, "$evaluators": {"beta": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es"]]}}}, {"source": "http", "flags": {}, "$evaluators": {"eu": {"in": [{"var": "region"}, ["es", "fr", "de"]]}}}], "expect": {"flags": {"x": {"origin": "config", "overrides": [], "definition": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}}}, "evaluators": {"beta": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de"]]}}}},
+ {"name": "an empty layer changes nothing", "kind": "compose", "layers": [{"source": "config", "flags": {"z": "blue"}}, {"source": "file", "flags": {}}], "expect": {"flags": {"z": {"origin": "config", "overrides": [], "definition": {"state": "ENABLED", "variants": {"blue": "blue"}, "defaultVariant": "blue"}}}}},
+ {"name": "a flag expires the day after its expires date (UTC)", "kind": "expiry", "flags": {"yesterday": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on", "metadata": {"expires": "2026-09-30"}}, "today": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on", "metadata": {"expires": "2026-10-01"}}, "later": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on", "metadata": {"expires": "2026-12-31"}}, "never": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "long-ago": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on", "metadata": {"expires": "2025-01-01"}}}, "expect": {"expired": ["long-ago", "yesterday"]}},
+ {"name": "a kill switch defined by shorthand is on", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "kill-switch", "type": "boolean", "default": false, "expect": {"value": true, "variant": "on", "reason": "STATIC"}},
+ {"name": "a beta user gets the release", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "new-checkout", "type": "boolean", "default": false, "targetingKey": "u-1", "context": {"roles": ["beta", "staff"]}, "expect": {"value": true, "variant": "on", "reason": "TARGETING_MATCH"}},
+ {"name": "a regular user falls through to the default variant", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "new-checkout", "type": "boolean", "default": true, "targetingKey": "u-2", "context": {"roles": ["staff"]}, "expect": {"value": false, "variant": "off", "reason": "DEFAULT"}},
+ {"name": "an anonymous user without roles falls through", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "new-checkout", "type": "boolean", "default": true, "expect": {"value": false, "variant": "off", "reason": "DEFAULT"}},
+ {"name": "a shared evaluator selects beta users", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "beta-segment", "type": "boolean", "default": false, "targetingKey": "u-3", "context": {"roles": ["beta"]}, "expect": {"value": true, "variant": "on", "reason": "TARGETING_MATCH"}},
+ {"name": "a shared evaluator rejects others", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "beta-segment", "type": "boolean", "default": true, "targetingKey": "u-4", "context": {"roles": []}, "expect": {"value": false, "variant": "off", "reason": "TARGETING_MATCH"}},
+ {"name": "a pro plan is entitled", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "pro-reports", "type": "boolean", "default": false, "targetingKey": "u-5", "context": {"tenant": "t1", "plan": "pro"}, "expect": {"value": true, "variant": "on", "reason": "TARGETING_MATCH"}},
+ {"name": "a free plan is not entitled", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "pro-reports", "type": "boolean", "default": true, "targetingKey": "u-6", "context": {"tenant": "t2", "plan": "free"}, "expect": {"value": false, "variant": "off", "reason": "TARGETING_MATCH"}},
+ {"name": "a missing plan is not entitled", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "pro-reports", "type": "boolean", "default": true, "targetingKey": "u-7", "context": {"tenant": "t3"}, "expect": {"value": false, "variant": "off", "reason": "TARGETING_MATCH"}},
+ {"name": "the pilot tenant gets the feature", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "tenant-pilot", "type": "boolean", "default": false, "targetingKey": "u-8", "context": {"tenant": "acme"}, "expect": {"value": true, "variant": "on", "reason": "TARGETING_MATCH"}},
+ {"name": "another tenant falls through", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "tenant-pilot", "type": "boolean", "default": true, "targetingKey": "u-9", "context": {"tenant": "globex"}, "expect": {"value": false, "variant": "off", "reason": "DEFAULT"}},
+ {"name": "experiment arm for alice", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "checkout-flow", "type": "string", "default": "fallback", "targetingKey": "alice@example.com", "expect": {"value": "v1", "variant": "control", "reason": "TARGETING_MATCH"}},
+ {"name": "experiment arm for bob", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "checkout-flow", "type": "string", "default": "fallback", "targetingKey": "bob@example.com", "expect": {"value": "v2", "variant": "treatment", "reason": "TARGETING_MATCH"}},
+ {"name": "experiment arm for carol", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "checkout-flow", "type": "string", "default": "fallback", "targetingKey": "carol@example.com", "expect": {"value": "v1", "variant": "control", "reason": "TARGETING_MATCH"}},
+ {"name": "experiment arm for dave", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "checkout-flow", "type": "string", "default": "fallback", "targetingKey": "dave@example.com", "expect": {"value": "v2", "variant": "treatment", "reason": "TARGETING_MATCH"}},
+ {"name": "experiment without a targeting key uses the default variant", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "checkout-flow", "type": "string", "default": "fallback", "expect": {"value": "v1", "variant": "control", "reason": "DEFAULT"}},
+ {"name": "10 percent rollout, user 42", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "rollout-10", "type": "boolean", "default": false, "targetingKey": "user-42", "expect": {"value": false, "variant": "off", "reason": "TARGETING_MATCH"}},
+ {"name": "10 percent rollout, user 7", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "rollout-10", "type": "boolean", "default": false, "targetingKey": "user-7", "expect": {"value": false, "variant": "off", "reason": "TARGETING_MATCH"}},
+ {"name": "10 percent rollout, user 4 is inside the 10 percent", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "rollout-10", "type": "boolean", "default": false, "targetingKey": "user-4", "expect": {"value": true, "variant": "on", "reason": "TARGETING_MATCH"}},
+ {"name": "semver gate passes a newer app", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "min-app-version", "type": "boolean", "default": false, "targetingKey": "u-10", "context": {"appVersion": "2.10.1"}, "expect": {"value": true, "variant": "on", "reason": "TARGETING_MATCH"}},
+ {"name": "semver gate rejects an older app", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "min-app-version", "type": "boolean", "default": true, "targetingKey": "u-11", "context": {"appVersion": "2.3.9"}, "expect": {"value": false, "variant": "off", "reason": "TARGETING_MATCH"}},
+ {"name": "semver gate accepts a v prefix", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "min-app-version", "type": "boolean", "default": false, "targetingKey": "u-12", "context": {"appVersion": "v2.4.0"}, "expect": {"value": true, "variant": "on", "reason": "TARGETING_MATCH"}},
+ {"name": "ends_with on an email", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "email-domain", "type": "boolean", "default": false, "targetingKey": "u-13", "context": {"email": "ana@firefly.dev"}, "expect": {"value": true, "variant": "on", "reason": "TARGETING_MATCH"}},
+ {"name": "ends_with rejects another domain", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "email-domain", "type": "boolean", "default": true, "targetingKey": "u-14", "context": {"email": "ana@example.org"}, "expect": {"value": false, "variant": "off", "reason": "TARGETING_MATCH"}},
+ {"name": "a disabled flag returns the caller default", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "paused", "type": "boolean", "default": false, "targetingKey": "u-15", "expect": {"value": false, "variant": null, "reason": "DISABLED"}},
+ {"name": "an integer flag for an admin", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "page-size", "type": "integer", "default": 25, "targetingKey": "u-16", "context": {"roles": ["admin"]}, "expect": {"value": 50, "variant": "large", "reason": "TARGETING_MATCH"}},
+ {"name": "an integer flag for others", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "page-size", "type": "integer", "default": 25, "targetingKey": "u-17", "context": {"roles": []}, "expect": {"value": 10, "variant": "small", "reason": "DEFAULT"}},
+ {"name": "a float flag without targeting", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "sample-rate", "type": "float", "default": 1.0, "expect": {"value": 0.05, "variant": "low", "reason": "STATIC"}},
+ {"name": "an object flag on staging", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "banner", "type": "object", "default": {}, "targetingKey": "u-18", "context": {"profiles": ["staging", "eu"]}, "expect": {"value": {"title": "50% off", "dismissible": false, "ttl": 3}, "variant": "promo", "reason": "TARGETING_MATCH"}},
+ {"name": "an object flag elsewhere", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "banner", "type": "object", "default": {}, "targetingKey": "u-19", "context": {"profiles": ["prod"]}, "expect": {"value": {"title": "Hello", "dismissible": true}, "variant": "plain", "reason": "DEFAULT"}},
+ {"name": "an evaluator over a list of regions", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "region-gate", "type": "boolean", "default": false, "targetingKey": "u-20", "context": {"region": "es"}, "expect": {"value": true, "variant": "on", "reason": "TARGETING_MATCH"}},
+ {"name": "an evaluator rejecting a region", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "region-gate", "type": "boolean", "default": true, "targetingKey": "u-21", "context": {"region": "us"}, "expect": {"value": false, "variant": "off", "reason": "TARGETING_MATCH"}},
+ {"name": "no default variant and no match returns the caller default", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "no-default", "type": "string", "default": "caller", "targetingKey": "u-22", "context": {"tenant": "globex"}, "expect": {"value": "caller", "variant": null, "reason": "DEFAULT"}},
+ {"name": "no default variant with a match", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "no-default", "type": "string", "default": "caller", "targetingKey": "u-23", "context": {"tenant": "acme"}, "expect": {"value": "B", "variant": "b", "reason": "TARGETING_MATCH"}},
+ {"name": "no default variant and no targeting returns the caller default", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "no-default-static", "type": "string", "default": "caller", "targetingKey": "u-25", "expect": {"value": "caller", "variant": null, "reason": "DEFAULT"}},
+ {"name": "targeting naming a variant that does not exist is a general error", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "bad-variant", "type": "string", "default": "caller", "targetingKey": "u-26", "expect": {"value": "caller", "variant": null, "reason": "ERROR", "errorCode": "GENERAL"}},
+ {"name": "an empty defaultVariant means no default variant, even when a variant is named \"\"", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "empty-default", "type": "string", "default": "caller", "targetingKey": "u-39", "expect": {"value": "caller", "variant": null, "reason": "DEFAULT"}},
+ {"name": "a backslash inside a shared evaluator is kept as written", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "windows-path", "type": "boolean", "default": false, "targetingKey": "u-27", "context": {"path": "C:\\temp\\new"}, "expect": {"value": true, "variant": "on", "reason": "TARGETING_MATCH"}},
+ {"name": "a backslash path that differs does not match", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "windows-path", "type": "boolean", "default": true, "targetingKey": "u-28", "context": {"path": "C:\\temp\\old"}, "expect": {"value": false, "variant": "off", "reason": "TARGETING_MATCH"}},
+ {"name": "an evaluator referencing an evaluator resolves transitively", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "nested-segment", "type": "boolean", "default": false, "targetingKey": "u-29", "context": {"roles": ["staff"], "region": "fr"}, "expect": {"value": true, "variant": "on", "reason": "TARGETING_MATCH"}},
+ {"name": "a transitive reference rejects a region outside the segment", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "nested-segment", "type": "boolean", "default": true, "targetingKey": "u-30", "context": {"roles": ["staff"], "region": "us"}, "expect": {"value": false, "variant": "off", "reason": "TARGETING_MATCH"}},
+ {"name": "transitive references resolve whatever the evaluator names sort as", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "nested-segment-reverse", "type": "boolean", "default": false, "targetingKey": "u-31", "context": {"roles": ["staff"], "region": "de"}, "expect": {"value": true, "variant": "on", "reason": "TARGETING_MATCH"}},
+ {"name": "a reference cycle is a parse error", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "cyclic-segment", "type": "boolean", "default": true, "targetingKey": "u-32", "expect": {"value": true, "variant": null, "reason": "ERROR", "errorCode": "PARSE_ERROR"}},
+ {"name": "an unknown flag", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "does-not-exist", "type": "boolean", "default": true, "targetingKey": "u-24", "expect": {"value": true, "variant": null, "reason": "ERROR", "errorCode": "FLAG_NOT_FOUND"}},
+ {"name": "a type mismatch returns the caller default", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "checkout-flow", "type": "boolean", "default": false, "targetingKey": "alice@example.com", "expect": {"value": false, "variant": null, "reason": "ERROR", "errorCode": "TYPE_MISMATCH"}},
+ {"name": "a fan-out of references within the expansion limit evaluates", "kind": "evaluate", "document": {"flags": {"fan-out-small": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "fan-5"}, "on", "off"]}}, "fan-out-huge": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "fan-20"}, "on", "off"]}}, "deep-chain-short": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "chain-20"}, "on", "off"]}}, "deep-chain-long": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "chain-200"}, "on", "off"]}}}, "$evaluators": {"fan-0": {"==": [{"var": "tier"}, "gold"]}, "fan-1": {"or": [{"$ref": "fan-0"}, {"$ref": "fan-0"}]}, "fan-2": {"or": [{"$ref": "fan-1"}, {"$ref": "fan-1"}]}, "fan-3": {"or": [{"$ref": "fan-2"}, {"$ref": "fan-2"}]}, "fan-4": {"or": [{"$ref": "fan-3"}, {"$ref": "fan-3"}]}, "fan-5": {"or": [{"$ref": "fan-4"}, {"$ref": "fan-4"}]}, "fan-6": {"or": [{"$ref": "fan-5"}, {"$ref": "fan-5"}]}, "fan-7": {"or": [{"$ref": "fan-6"}, {"$ref": "fan-6"}]}, "fan-8": {"or": [{"$ref": "fan-7"}, {"$ref": "fan-7"}]}, "fan-9": {"or": [{"$ref": "fan-8"}, {"$ref": "fan-8"}]}, "fan-10": {"or": [{"$ref": "fan-9"}, {"$ref": "fan-9"}]}, "fan-11": {"or": [{"$ref": "fan-10"}, {"$ref": "fan-10"}]}, "fan-12": {"or": [{"$ref": "fan-11"}, {"$ref": "fan-11"}]}, "fan-13": {"or": [{"$ref": "fan-12"}, {"$ref": "fan-12"}]}, "fan-14": {"or": [{"$ref": "fan-13"}, {"$ref": "fan-13"}]}, "fan-15": {"or": [{"$ref": "fan-14"}, {"$ref": "fan-14"}]}, "fan-16": {"or": [{"$ref": "fan-15"}, {"$ref": "fan-15"}]}, "fan-17": {"or": [{"$ref": "fan-16"}, {"$ref": "fan-16"}]}, "fan-18": {"or": [{"$ref": "fan-17"}, {"$ref": "fan-17"}]}, "fan-19": {"or": [{"$ref": "fan-18"}, {"$ref": "fan-18"}]}, "fan-20": {"or": [{"$ref": "fan-19"}, {"$ref": "fan-19"}]}, "chain-0": {"==": [{"var": "tier"}, "gold"]}, "chain-1": {"and": [{"$ref": "chain-0"}]}, "chain-2": {"and": [{"$ref": "chain-1"}]}, "chain-3": {"and": [{"$ref": "chain-2"}]}, "chain-4": {"and": [{"$ref": "chain-3"}]}, "chain-5": {"and": [{"$ref": "chain-4"}]}, "chain-6": {"and": [{"$ref": "chain-5"}]}, "chain-7": {"and": [{"$ref": "chain-6"}]}, "chain-8": {"and": [{"$ref": "chain-7"}]}, "chain-9": {"and": [{"$ref": "chain-8"}]}, "chain-10": {"and": [{"$ref": "chain-9"}]}, "chain-11": {"and": [{"$ref": "chain-10"}]}, "chain-12": {"and": [{"$ref": "chain-11"}]}, "chain-13": {"and": [{"$ref": "chain-12"}]}, "chain-14": {"and": [{"$ref": "chain-13"}]}, "chain-15": {"and": [{"$ref": "chain-14"}]}, "chain-16": {"and": [{"$ref": "chain-15"}]}, "chain-17": {"and": [{"$ref": "chain-16"}]}, "chain-18": {"and": [{"$ref": "chain-17"}]}, "chain-19": {"and": [{"$ref": "chain-18"}]}, "chain-20": {"and": [{"$ref": "chain-19"}]}, "chain-21": {"and": [{"$ref": "chain-20"}]}, "chain-22": {"and": [{"$ref": "chain-21"}]}, "chain-23": {"and": [{"$ref": "chain-22"}]}, "chain-24": {"and": [{"$ref": "chain-23"}]}, "chain-25": {"and": [{"$ref": "chain-24"}]}, "chain-26": {"and": [{"$ref": "chain-25"}]}, "chain-27": {"and": [{"$ref": "chain-26"}]}, "chain-28": {"and": [{"$ref": "chain-27"}]}, "chain-29": {"and": [{"$ref": "chain-28"}]}, "chain-30": {"and": [{"$ref": "chain-29"}]}, "chain-31": {"and": [{"$ref": "chain-30"}]}, "chain-32": {"and": [{"$ref": "chain-31"}]}, "chain-33": {"and": [{"$ref": "chain-32"}]}, "chain-34": {"and": [{"$ref": "chain-33"}]}, "chain-35": {"and": [{"$ref": "chain-34"}]}, "chain-36": {"and": [{"$ref": "chain-35"}]}, "chain-37": {"and": [{"$ref": "chain-36"}]}, "chain-38": {"and": [{"$ref": "chain-37"}]}, "chain-39": {"and": [{"$ref": "chain-38"}]}, "chain-40": {"and": [{"$ref": "chain-39"}]}, "chain-41": {"and": [{"$ref": "chain-40"}]}, "chain-42": {"and": [{"$ref": "chain-41"}]}, "chain-43": {"and": [{"$ref": "chain-42"}]}, "chain-44": {"and": [{"$ref": "chain-43"}]}, "chain-45": {"and": [{"$ref": "chain-44"}]}, "chain-46": {"and": [{"$ref": "chain-45"}]}, "chain-47": {"and": [{"$ref": "chain-46"}]}, "chain-48": {"and": [{"$ref": "chain-47"}]}, "chain-49": {"and": [{"$ref": "chain-48"}]}, "chain-50": {"and": [{"$ref": "chain-49"}]}, "chain-51": {"and": [{"$ref": "chain-50"}]}, "chain-52": {"and": [{"$ref": "chain-51"}]}, "chain-53": {"and": [{"$ref": "chain-52"}]}, "chain-54": {"and": [{"$ref": "chain-53"}]}, "chain-55": {"and": [{"$ref": "chain-54"}]}, "chain-56": {"and": [{"$ref": "chain-55"}]}, "chain-57": {"and": [{"$ref": "chain-56"}]}, "chain-58": {"and": [{"$ref": "chain-57"}]}, "chain-59": {"and": [{"$ref": "chain-58"}]}, "chain-60": {"and": [{"$ref": "chain-59"}]}, "chain-61": {"and": [{"$ref": "chain-60"}]}, "chain-62": {"and": [{"$ref": "chain-61"}]}, "chain-63": {"and": [{"$ref": "chain-62"}]}, "chain-64": {"and": [{"$ref": "chain-63"}]}, "chain-65": {"and": [{"$ref": "chain-64"}]}, "chain-66": {"and": [{"$ref": "chain-65"}]}, "chain-67": {"and": [{"$ref": "chain-66"}]}, "chain-68": {"and": [{"$ref": "chain-67"}]}, "chain-69": {"and": [{"$ref": "chain-68"}]}, "chain-70": {"and": [{"$ref": "chain-69"}]}, "chain-71": {"and": [{"$ref": "chain-70"}]}, "chain-72": {"and": [{"$ref": "chain-71"}]}, "chain-73": {"and": [{"$ref": "chain-72"}]}, "chain-74": {"and": [{"$ref": "chain-73"}]}, "chain-75": {"and": [{"$ref": "chain-74"}]}, "chain-76": {"and": [{"$ref": "chain-75"}]}, "chain-77": {"and": [{"$ref": "chain-76"}]}, "chain-78": {"and": [{"$ref": "chain-77"}]}, "chain-79": {"and": [{"$ref": "chain-78"}]}, "chain-80": {"and": [{"$ref": "chain-79"}]}, "chain-81": {"and": [{"$ref": "chain-80"}]}, "chain-82": {"and": [{"$ref": "chain-81"}]}, "chain-83": {"and": [{"$ref": "chain-82"}]}, "chain-84": {"and": [{"$ref": "chain-83"}]}, "chain-85": {"and": [{"$ref": "chain-84"}]}, "chain-86": {"and": [{"$ref": "chain-85"}]}, "chain-87": {"and": [{"$ref": "chain-86"}]}, "chain-88": {"and": [{"$ref": "chain-87"}]}, "chain-89": {"and": [{"$ref": "chain-88"}]}, "chain-90": {"and": [{"$ref": "chain-89"}]}, "chain-91": {"and": [{"$ref": "chain-90"}]}, "chain-92": {"and": [{"$ref": "chain-91"}]}, "chain-93": {"and": [{"$ref": "chain-92"}]}, "chain-94": {"and": [{"$ref": "chain-93"}]}, "chain-95": {"and": [{"$ref": "chain-94"}]}, "chain-96": {"and": [{"$ref": "chain-95"}]}, "chain-97": {"and": [{"$ref": "chain-96"}]}, "chain-98": {"and": [{"$ref": "chain-97"}]}, "chain-99": {"and": [{"$ref": "chain-98"}]}, "chain-100": {"and": [{"$ref": "chain-99"}]}, "chain-101": {"and": [{"$ref": "chain-100"}]}, "chain-102": {"and": [{"$ref": "chain-101"}]}, "chain-103": {"and": [{"$ref": "chain-102"}]}, "chain-104": {"and": [{"$ref": "chain-103"}]}, "chain-105": {"and": [{"$ref": "chain-104"}]}, "chain-106": {"and": [{"$ref": "chain-105"}]}, "chain-107": {"and": [{"$ref": "chain-106"}]}, "chain-108": {"and": [{"$ref": "chain-107"}]}, "chain-109": {"and": [{"$ref": "chain-108"}]}, "chain-110": {"and": [{"$ref": "chain-109"}]}, "chain-111": {"and": [{"$ref": "chain-110"}]}, "chain-112": {"and": [{"$ref": "chain-111"}]}, "chain-113": {"and": [{"$ref": "chain-112"}]}, "chain-114": {"and": [{"$ref": "chain-113"}]}, "chain-115": {"and": [{"$ref": "chain-114"}]}, "chain-116": {"and": [{"$ref": "chain-115"}]}, "chain-117": {"and": [{"$ref": "chain-116"}]}, "chain-118": {"and": [{"$ref": "chain-117"}]}, "chain-119": {"and": [{"$ref": "chain-118"}]}, "chain-120": {"and": [{"$ref": "chain-119"}]}, "chain-121": {"and": [{"$ref": "chain-120"}]}, "chain-122": {"and": [{"$ref": "chain-121"}]}, "chain-123": {"and": [{"$ref": "chain-122"}]}, "chain-124": {"and": [{"$ref": "chain-123"}]}, "chain-125": {"and": [{"$ref": "chain-124"}]}, "chain-126": {"and": [{"$ref": "chain-125"}]}, "chain-127": {"and": [{"$ref": "chain-126"}]}, "chain-128": {"and": [{"$ref": "chain-127"}]}, "chain-129": {"and": [{"$ref": "chain-128"}]}, "chain-130": {"and": [{"$ref": "chain-129"}]}, "chain-131": {"and": [{"$ref": "chain-130"}]}, "chain-132": {"and": [{"$ref": "chain-131"}]}, "chain-133": {"and": [{"$ref": "chain-132"}]}, "chain-134": {"and": [{"$ref": "chain-133"}]}, "chain-135": {"and": [{"$ref": "chain-134"}]}, "chain-136": {"and": [{"$ref": "chain-135"}]}, "chain-137": {"and": [{"$ref": "chain-136"}]}, "chain-138": {"and": [{"$ref": "chain-137"}]}, "chain-139": {"and": [{"$ref": "chain-138"}]}, "chain-140": {"and": [{"$ref": "chain-139"}]}, "chain-141": {"and": [{"$ref": "chain-140"}]}, "chain-142": {"and": [{"$ref": "chain-141"}]}, "chain-143": {"and": [{"$ref": "chain-142"}]}, "chain-144": {"and": [{"$ref": "chain-143"}]}, "chain-145": {"and": [{"$ref": "chain-144"}]}, "chain-146": {"and": [{"$ref": "chain-145"}]}, "chain-147": {"and": [{"$ref": "chain-146"}]}, "chain-148": {"and": [{"$ref": "chain-147"}]}, "chain-149": {"and": [{"$ref": "chain-148"}]}, "chain-150": {"and": [{"$ref": "chain-149"}]}, "chain-151": {"and": [{"$ref": "chain-150"}]}, "chain-152": {"and": [{"$ref": "chain-151"}]}, "chain-153": {"and": [{"$ref": "chain-152"}]}, "chain-154": {"and": [{"$ref": "chain-153"}]}, "chain-155": {"and": [{"$ref": "chain-154"}]}, "chain-156": {"and": [{"$ref": "chain-155"}]}, "chain-157": {"and": [{"$ref": "chain-156"}]}, "chain-158": {"and": [{"$ref": "chain-157"}]}, "chain-159": {"and": [{"$ref": "chain-158"}]}, "chain-160": {"and": [{"$ref": "chain-159"}]}, "chain-161": {"and": [{"$ref": "chain-160"}]}, "chain-162": {"and": [{"$ref": "chain-161"}]}, "chain-163": {"and": [{"$ref": "chain-162"}]}, "chain-164": {"and": [{"$ref": "chain-163"}]}, "chain-165": {"and": [{"$ref": "chain-164"}]}, "chain-166": {"and": [{"$ref": "chain-165"}]}, "chain-167": {"and": [{"$ref": "chain-166"}]}, "chain-168": {"and": [{"$ref": "chain-167"}]}, "chain-169": {"and": [{"$ref": "chain-168"}]}, "chain-170": {"and": [{"$ref": "chain-169"}]}, "chain-171": {"and": [{"$ref": "chain-170"}]}, "chain-172": {"and": [{"$ref": "chain-171"}]}, "chain-173": {"and": [{"$ref": "chain-172"}]}, "chain-174": {"and": [{"$ref": "chain-173"}]}, "chain-175": {"and": [{"$ref": "chain-174"}]}, "chain-176": {"and": [{"$ref": "chain-175"}]}, "chain-177": {"and": [{"$ref": "chain-176"}]}, "chain-178": {"and": [{"$ref": "chain-177"}]}, "chain-179": {"and": [{"$ref": "chain-178"}]}, "chain-180": {"and": [{"$ref": "chain-179"}]}, "chain-181": {"and": [{"$ref": "chain-180"}]}, "chain-182": {"and": [{"$ref": "chain-181"}]}, "chain-183": {"and": [{"$ref": "chain-182"}]}, "chain-184": {"and": [{"$ref": "chain-183"}]}, "chain-185": {"and": [{"$ref": "chain-184"}]}, "chain-186": {"and": [{"$ref": "chain-185"}]}, "chain-187": {"and": [{"$ref": "chain-186"}]}, "chain-188": {"and": [{"$ref": "chain-187"}]}, "chain-189": {"and": [{"$ref": "chain-188"}]}, "chain-190": {"and": [{"$ref": "chain-189"}]}, "chain-191": {"and": [{"$ref": "chain-190"}]}, "chain-192": {"and": [{"$ref": "chain-191"}]}, "chain-193": {"and": [{"$ref": "chain-192"}]}, "chain-194": {"and": [{"$ref": "chain-193"}]}, "chain-195": {"and": [{"$ref": "chain-194"}]}, "chain-196": {"and": [{"$ref": "chain-195"}]}, "chain-197": {"and": [{"$ref": "chain-196"}]}, "chain-198": {"and": [{"$ref": "chain-197"}]}, "chain-199": {"and": [{"$ref": "chain-198"}]}, "chain-200": {"and": [{"$ref": "chain-199"}]}}}, "flag": "fan-out-small", "type": "boolean", "default": false, "targetingKey": "u-33", "context": {"tier": "gold"}, "expect": {"value": true, "variant": "on", "reason": "TARGETING_MATCH"}},
+ {"name": "a fan-out of references beyond 10 000 values is a parse error", "kind": "evaluate", "document": {"flags": {"fan-out-small": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "fan-5"}, "on", "off"]}}, "fan-out-huge": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "fan-20"}, "on", "off"]}}, "deep-chain-short": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "chain-20"}, "on", "off"]}}, "deep-chain-long": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "chain-200"}, "on", "off"]}}}, "$evaluators": {"fan-0": {"==": [{"var": "tier"}, "gold"]}, "fan-1": {"or": [{"$ref": "fan-0"}, {"$ref": "fan-0"}]}, "fan-2": {"or": [{"$ref": "fan-1"}, {"$ref": "fan-1"}]}, "fan-3": {"or": [{"$ref": "fan-2"}, {"$ref": "fan-2"}]}, "fan-4": {"or": [{"$ref": "fan-3"}, {"$ref": "fan-3"}]}, "fan-5": {"or": [{"$ref": "fan-4"}, {"$ref": "fan-4"}]}, "fan-6": {"or": [{"$ref": "fan-5"}, {"$ref": "fan-5"}]}, "fan-7": {"or": [{"$ref": "fan-6"}, {"$ref": "fan-6"}]}, "fan-8": {"or": [{"$ref": "fan-7"}, {"$ref": "fan-7"}]}, "fan-9": {"or": [{"$ref": "fan-8"}, {"$ref": "fan-8"}]}, "fan-10": {"or": [{"$ref": "fan-9"}, {"$ref": "fan-9"}]}, "fan-11": {"or": [{"$ref": "fan-10"}, {"$ref": "fan-10"}]}, "fan-12": {"or": [{"$ref": "fan-11"}, {"$ref": "fan-11"}]}, "fan-13": {"or": [{"$ref": "fan-12"}, {"$ref": "fan-12"}]}, "fan-14": {"or": [{"$ref": "fan-13"}, {"$ref": "fan-13"}]}, "fan-15": {"or": [{"$ref": "fan-14"}, {"$ref": "fan-14"}]}, "fan-16": {"or": [{"$ref": "fan-15"}, {"$ref": "fan-15"}]}, "fan-17": {"or": [{"$ref": "fan-16"}, {"$ref": "fan-16"}]}, "fan-18": {"or": [{"$ref": "fan-17"}, {"$ref": "fan-17"}]}, "fan-19": {"or": [{"$ref": "fan-18"}, {"$ref": "fan-18"}]}, "fan-20": {"or": [{"$ref": "fan-19"}, {"$ref": "fan-19"}]}, "chain-0": {"==": [{"var": "tier"}, "gold"]}, "chain-1": {"and": [{"$ref": "chain-0"}]}, "chain-2": {"and": [{"$ref": "chain-1"}]}, "chain-3": {"and": [{"$ref": "chain-2"}]}, "chain-4": {"and": [{"$ref": "chain-3"}]}, "chain-5": {"and": [{"$ref": "chain-4"}]}, "chain-6": {"and": [{"$ref": "chain-5"}]}, "chain-7": {"and": [{"$ref": "chain-6"}]}, "chain-8": {"and": [{"$ref": "chain-7"}]}, "chain-9": {"and": [{"$ref": "chain-8"}]}, "chain-10": {"and": [{"$ref": "chain-9"}]}, "chain-11": {"and": [{"$ref": "chain-10"}]}, "chain-12": {"and": [{"$ref": "chain-11"}]}, "chain-13": {"and": [{"$ref": "chain-12"}]}, "chain-14": {"and": [{"$ref": "chain-13"}]}, "chain-15": {"and": [{"$ref": "chain-14"}]}, "chain-16": {"and": [{"$ref": "chain-15"}]}, "chain-17": {"and": [{"$ref": "chain-16"}]}, "chain-18": {"and": [{"$ref": "chain-17"}]}, "chain-19": {"and": [{"$ref": "chain-18"}]}, "chain-20": {"and": [{"$ref": "chain-19"}]}, "chain-21": {"and": [{"$ref": "chain-20"}]}, "chain-22": {"and": [{"$ref": "chain-21"}]}, "chain-23": {"and": [{"$ref": "chain-22"}]}, "chain-24": {"and": [{"$ref": "chain-23"}]}, "chain-25": {"and": [{"$ref": "chain-24"}]}, "chain-26": {"and": [{"$ref": "chain-25"}]}, "chain-27": {"and": [{"$ref": "chain-26"}]}, "chain-28": {"and": [{"$ref": "chain-27"}]}, "chain-29": {"and": [{"$ref": "chain-28"}]}, "chain-30": {"and": [{"$ref": "chain-29"}]}, "chain-31": {"and": [{"$ref": "chain-30"}]}, "chain-32": {"and": [{"$ref": "chain-31"}]}, "chain-33": {"and": [{"$ref": "chain-32"}]}, "chain-34": {"and": [{"$ref": "chain-33"}]}, "chain-35": {"and": [{"$ref": "chain-34"}]}, "chain-36": {"and": [{"$ref": "chain-35"}]}, "chain-37": {"and": [{"$ref": "chain-36"}]}, "chain-38": {"and": [{"$ref": "chain-37"}]}, "chain-39": {"and": [{"$ref": "chain-38"}]}, "chain-40": {"and": [{"$ref": "chain-39"}]}, "chain-41": {"and": [{"$ref": "chain-40"}]}, "chain-42": {"and": [{"$ref": "chain-41"}]}, "chain-43": {"and": [{"$ref": "chain-42"}]}, "chain-44": {"and": [{"$ref": "chain-43"}]}, "chain-45": {"and": [{"$ref": "chain-44"}]}, "chain-46": {"and": [{"$ref": "chain-45"}]}, "chain-47": {"and": [{"$ref": "chain-46"}]}, "chain-48": {"and": [{"$ref": "chain-47"}]}, "chain-49": {"and": [{"$ref": "chain-48"}]}, "chain-50": {"and": [{"$ref": "chain-49"}]}, "chain-51": {"and": [{"$ref": "chain-50"}]}, "chain-52": {"and": [{"$ref": "chain-51"}]}, "chain-53": {"and": [{"$ref": "chain-52"}]}, "chain-54": {"and": [{"$ref": "chain-53"}]}, "chain-55": {"and": [{"$ref": "chain-54"}]}, "chain-56": {"and": [{"$ref": "chain-55"}]}, "chain-57": {"and": [{"$ref": "chain-56"}]}, "chain-58": {"and": [{"$ref": "chain-57"}]}, "chain-59": {"and": [{"$ref": "chain-58"}]}, "chain-60": {"and": [{"$ref": "chain-59"}]}, "chain-61": {"and": [{"$ref": "chain-60"}]}, "chain-62": {"and": [{"$ref": "chain-61"}]}, "chain-63": {"and": [{"$ref": "chain-62"}]}, "chain-64": {"and": [{"$ref": "chain-63"}]}, "chain-65": {"and": [{"$ref": "chain-64"}]}, "chain-66": {"and": [{"$ref": "chain-65"}]}, "chain-67": {"and": [{"$ref": "chain-66"}]}, "chain-68": {"and": [{"$ref": "chain-67"}]}, "chain-69": {"and": [{"$ref": "chain-68"}]}, "chain-70": {"and": [{"$ref": "chain-69"}]}, "chain-71": {"and": [{"$ref": "chain-70"}]}, "chain-72": {"and": [{"$ref": "chain-71"}]}, "chain-73": {"and": [{"$ref": "chain-72"}]}, "chain-74": {"and": [{"$ref": "chain-73"}]}, "chain-75": {"and": [{"$ref": "chain-74"}]}, "chain-76": {"and": [{"$ref": "chain-75"}]}, "chain-77": {"and": [{"$ref": "chain-76"}]}, "chain-78": {"and": [{"$ref": "chain-77"}]}, "chain-79": {"and": [{"$ref": "chain-78"}]}, "chain-80": {"and": [{"$ref": "chain-79"}]}, "chain-81": {"and": [{"$ref": "chain-80"}]}, "chain-82": {"and": [{"$ref": "chain-81"}]}, "chain-83": {"and": [{"$ref": "chain-82"}]}, "chain-84": {"and": [{"$ref": "chain-83"}]}, "chain-85": {"and": [{"$ref": "chain-84"}]}, "chain-86": {"and": [{"$ref": "chain-85"}]}, "chain-87": {"and": [{"$ref": "chain-86"}]}, "chain-88": {"and": [{"$ref": "chain-87"}]}, "chain-89": {"and": [{"$ref": "chain-88"}]}, "chain-90": {"and": [{"$ref": "chain-89"}]}, "chain-91": {"and": [{"$ref": "chain-90"}]}, "chain-92": {"and": [{"$ref": "chain-91"}]}, "chain-93": {"and": [{"$ref": "chain-92"}]}, "chain-94": {"and": [{"$ref": "chain-93"}]}, "chain-95": {"and": [{"$ref": "chain-94"}]}, "chain-96": {"and": [{"$ref": "chain-95"}]}, "chain-97": {"and": [{"$ref": "chain-96"}]}, "chain-98": {"and": [{"$ref": "chain-97"}]}, "chain-99": {"and": [{"$ref": "chain-98"}]}, "chain-100": {"and": [{"$ref": "chain-99"}]}, "chain-101": {"and": [{"$ref": "chain-100"}]}, "chain-102": {"and": [{"$ref": "chain-101"}]}, "chain-103": {"and": [{"$ref": "chain-102"}]}, "chain-104": {"and": [{"$ref": "chain-103"}]}, "chain-105": {"and": [{"$ref": "chain-104"}]}, "chain-106": {"and": [{"$ref": "chain-105"}]}, "chain-107": {"and": [{"$ref": "chain-106"}]}, "chain-108": {"and": [{"$ref": "chain-107"}]}, "chain-109": {"and": [{"$ref": "chain-108"}]}, "chain-110": {"and": [{"$ref": "chain-109"}]}, "chain-111": {"and": [{"$ref": "chain-110"}]}, "chain-112": {"and": [{"$ref": "chain-111"}]}, "chain-113": {"and": [{"$ref": "chain-112"}]}, "chain-114": {"and": [{"$ref": "chain-113"}]}, "chain-115": {"and": [{"$ref": "chain-114"}]}, "chain-116": {"and": [{"$ref": "chain-115"}]}, "chain-117": {"and": [{"$ref": "chain-116"}]}, "chain-118": {"and": [{"$ref": "chain-117"}]}, "chain-119": {"and": [{"$ref": "chain-118"}]}, "chain-120": {"and": [{"$ref": "chain-119"}]}, "chain-121": {"and": [{"$ref": "chain-120"}]}, "chain-122": {"and": [{"$ref": "chain-121"}]}, "chain-123": {"and": [{"$ref": "chain-122"}]}, "chain-124": {"and": [{"$ref": "chain-123"}]}, "chain-125": {"and": [{"$ref": "chain-124"}]}, "chain-126": {"and": [{"$ref": "chain-125"}]}, "chain-127": {"and": [{"$ref": "chain-126"}]}, "chain-128": {"and": [{"$ref": "chain-127"}]}, "chain-129": {"and": [{"$ref": "chain-128"}]}, "chain-130": {"and": [{"$ref": "chain-129"}]}, "chain-131": {"and": [{"$ref": "chain-130"}]}, "chain-132": {"and": [{"$ref": "chain-131"}]}, "chain-133": {"and": [{"$ref": "chain-132"}]}, "chain-134": {"and": [{"$ref": "chain-133"}]}, "chain-135": {"and": [{"$ref": "chain-134"}]}, "chain-136": {"and": [{"$ref": "chain-135"}]}, "chain-137": {"and": [{"$ref": "chain-136"}]}, "chain-138": {"and": [{"$ref": "chain-137"}]}, "chain-139": {"and": [{"$ref": "chain-138"}]}, "chain-140": {"and": [{"$ref": "chain-139"}]}, "chain-141": {"and": [{"$ref": "chain-140"}]}, "chain-142": {"and": [{"$ref": "chain-141"}]}, "chain-143": {"and": [{"$ref": "chain-142"}]}, "chain-144": {"and": [{"$ref": "chain-143"}]}, "chain-145": {"and": [{"$ref": "chain-144"}]}, "chain-146": {"and": [{"$ref": "chain-145"}]}, "chain-147": {"and": [{"$ref": "chain-146"}]}, "chain-148": {"and": [{"$ref": "chain-147"}]}, "chain-149": {"and": [{"$ref": "chain-148"}]}, "chain-150": {"and": [{"$ref": "chain-149"}]}, "chain-151": {"and": [{"$ref": "chain-150"}]}, "chain-152": {"and": [{"$ref": "chain-151"}]}, "chain-153": {"and": [{"$ref": "chain-152"}]}, "chain-154": {"and": [{"$ref": "chain-153"}]}, "chain-155": {"and": [{"$ref": "chain-154"}]}, "chain-156": {"and": [{"$ref": "chain-155"}]}, "chain-157": {"and": [{"$ref": "chain-156"}]}, "chain-158": {"and": [{"$ref": "chain-157"}]}, "chain-159": {"and": [{"$ref": "chain-158"}]}, "chain-160": {"and": [{"$ref": "chain-159"}]}, "chain-161": {"and": [{"$ref": "chain-160"}]}, "chain-162": {"and": [{"$ref": "chain-161"}]}, "chain-163": {"and": [{"$ref": "chain-162"}]}, "chain-164": {"and": [{"$ref": "chain-163"}]}, "chain-165": {"and": [{"$ref": "chain-164"}]}, "chain-166": {"and": [{"$ref": "chain-165"}]}, "chain-167": {"and": [{"$ref": "chain-166"}]}, "chain-168": {"and": [{"$ref": "chain-167"}]}, "chain-169": {"and": [{"$ref": "chain-168"}]}, "chain-170": {"and": [{"$ref": "chain-169"}]}, "chain-171": {"and": [{"$ref": "chain-170"}]}, "chain-172": {"and": [{"$ref": "chain-171"}]}, "chain-173": {"and": [{"$ref": "chain-172"}]}, "chain-174": {"and": [{"$ref": "chain-173"}]}, "chain-175": {"and": [{"$ref": "chain-174"}]}, "chain-176": {"and": [{"$ref": "chain-175"}]}, "chain-177": {"and": [{"$ref": "chain-176"}]}, "chain-178": {"and": [{"$ref": "chain-177"}]}, "chain-179": {"and": [{"$ref": "chain-178"}]}, "chain-180": {"and": [{"$ref": "chain-179"}]}, "chain-181": {"and": [{"$ref": "chain-180"}]}, "chain-182": {"and": [{"$ref": "chain-181"}]}, "chain-183": {"and": [{"$ref": "chain-182"}]}, "chain-184": {"and": [{"$ref": "chain-183"}]}, "chain-185": {"and": [{"$ref": "chain-184"}]}, "chain-186": {"and": [{"$ref": "chain-185"}]}, "chain-187": {"and": [{"$ref": "chain-186"}]}, "chain-188": {"and": [{"$ref": "chain-187"}]}, "chain-189": {"and": [{"$ref": "chain-188"}]}, "chain-190": {"and": [{"$ref": "chain-189"}]}, "chain-191": {"and": [{"$ref": "chain-190"}]}, "chain-192": {"and": [{"$ref": "chain-191"}]}, "chain-193": {"and": [{"$ref": "chain-192"}]}, "chain-194": {"and": [{"$ref": "chain-193"}]}, "chain-195": {"and": [{"$ref": "chain-194"}]}, "chain-196": {"and": [{"$ref": "chain-195"}]}, "chain-197": {"and": [{"$ref": "chain-196"}]}, "chain-198": {"and": [{"$ref": "chain-197"}]}, "chain-199": {"and": [{"$ref": "chain-198"}]}, "chain-200": {"and": [{"$ref": "chain-199"}]}}}, "flag": "fan-out-huge", "type": "boolean", "default": true, "targetingKey": "u-34", "context": {"tier": "gold"}, "expect": {"value": true, "variant": null, "reason": "ERROR", "errorCode": "PARSE_ERROR"}},
+ {"name": "a chain of references within 128 nesting levels evaluates", "kind": "evaluate", "document": {"flags": {"fan-out-small": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "fan-5"}, "on", "off"]}}, "fan-out-huge": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "fan-20"}, "on", "off"]}}, "deep-chain-short": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "chain-20"}, "on", "off"]}}, "deep-chain-long": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "chain-200"}, "on", "off"]}}}, "$evaluators": {"fan-0": {"==": [{"var": "tier"}, "gold"]}, "fan-1": {"or": [{"$ref": "fan-0"}, {"$ref": "fan-0"}]}, "fan-2": {"or": [{"$ref": "fan-1"}, {"$ref": "fan-1"}]}, "fan-3": {"or": [{"$ref": "fan-2"}, {"$ref": "fan-2"}]}, "fan-4": {"or": [{"$ref": "fan-3"}, {"$ref": "fan-3"}]}, "fan-5": {"or": [{"$ref": "fan-4"}, {"$ref": "fan-4"}]}, "fan-6": {"or": [{"$ref": "fan-5"}, {"$ref": "fan-5"}]}, "fan-7": {"or": [{"$ref": "fan-6"}, {"$ref": "fan-6"}]}, "fan-8": {"or": [{"$ref": "fan-7"}, {"$ref": "fan-7"}]}, "fan-9": {"or": [{"$ref": "fan-8"}, {"$ref": "fan-8"}]}, "fan-10": {"or": [{"$ref": "fan-9"}, {"$ref": "fan-9"}]}, "fan-11": {"or": [{"$ref": "fan-10"}, {"$ref": "fan-10"}]}, "fan-12": {"or": [{"$ref": "fan-11"}, {"$ref": "fan-11"}]}, "fan-13": {"or": [{"$ref": "fan-12"}, {"$ref": "fan-12"}]}, "fan-14": {"or": [{"$ref": "fan-13"}, {"$ref": "fan-13"}]}, "fan-15": {"or": [{"$ref": "fan-14"}, {"$ref": "fan-14"}]}, "fan-16": {"or": [{"$ref": "fan-15"}, {"$ref": "fan-15"}]}, "fan-17": {"or": [{"$ref": "fan-16"}, {"$ref": "fan-16"}]}, "fan-18": {"or": [{"$ref": "fan-17"}, {"$ref": "fan-17"}]}, "fan-19": {"or": [{"$ref": "fan-18"}, {"$ref": "fan-18"}]}, "fan-20": {"or": [{"$ref": "fan-19"}, {"$ref": "fan-19"}]}, "chain-0": {"==": [{"var": "tier"}, "gold"]}, "chain-1": {"and": [{"$ref": "chain-0"}]}, "chain-2": {"and": [{"$ref": "chain-1"}]}, "chain-3": {"and": [{"$ref": "chain-2"}]}, "chain-4": {"and": [{"$ref": "chain-3"}]}, "chain-5": {"and": [{"$ref": "chain-4"}]}, "chain-6": {"and": [{"$ref": "chain-5"}]}, "chain-7": {"and": [{"$ref": "chain-6"}]}, "chain-8": {"and": [{"$ref": "chain-7"}]}, "chain-9": {"and": [{"$ref": "chain-8"}]}, "chain-10": {"and": [{"$ref": "chain-9"}]}, "chain-11": {"and": [{"$ref": "chain-10"}]}, "chain-12": {"and": [{"$ref": "chain-11"}]}, "chain-13": {"and": [{"$ref": "chain-12"}]}, "chain-14": {"and": [{"$ref": "chain-13"}]}, "chain-15": {"and": [{"$ref": "chain-14"}]}, "chain-16": {"and": [{"$ref": "chain-15"}]}, "chain-17": {"and": [{"$ref": "chain-16"}]}, "chain-18": {"and": [{"$ref": "chain-17"}]}, "chain-19": {"and": [{"$ref": "chain-18"}]}, "chain-20": {"and": [{"$ref": "chain-19"}]}, "chain-21": {"and": [{"$ref": "chain-20"}]}, "chain-22": {"and": [{"$ref": "chain-21"}]}, "chain-23": {"and": [{"$ref": "chain-22"}]}, "chain-24": {"and": [{"$ref": "chain-23"}]}, "chain-25": {"and": [{"$ref": "chain-24"}]}, "chain-26": {"and": [{"$ref": "chain-25"}]}, "chain-27": {"and": [{"$ref": "chain-26"}]}, "chain-28": {"and": [{"$ref": "chain-27"}]}, "chain-29": {"and": [{"$ref": "chain-28"}]}, "chain-30": {"and": [{"$ref": "chain-29"}]}, "chain-31": {"and": [{"$ref": "chain-30"}]}, "chain-32": {"and": [{"$ref": "chain-31"}]}, "chain-33": {"and": [{"$ref": "chain-32"}]}, "chain-34": {"and": [{"$ref": "chain-33"}]}, "chain-35": {"and": [{"$ref": "chain-34"}]}, "chain-36": {"and": [{"$ref": "chain-35"}]}, "chain-37": {"and": [{"$ref": "chain-36"}]}, "chain-38": {"and": [{"$ref": "chain-37"}]}, "chain-39": {"and": [{"$ref": "chain-38"}]}, "chain-40": {"and": [{"$ref": "chain-39"}]}, "chain-41": {"and": [{"$ref": "chain-40"}]}, "chain-42": {"and": [{"$ref": "chain-41"}]}, "chain-43": {"and": [{"$ref": "chain-42"}]}, "chain-44": {"and": [{"$ref": "chain-43"}]}, "chain-45": {"and": [{"$ref": "chain-44"}]}, "chain-46": {"and": [{"$ref": "chain-45"}]}, "chain-47": {"and": [{"$ref": "chain-46"}]}, "chain-48": {"and": [{"$ref": "chain-47"}]}, "chain-49": {"and": [{"$ref": "chain-48"}]}, "chain-50": {"and": [{"$ref": "chain-49"}]}, "chain-51": {"and": [{"$ref": "chain-50"}]}, "chain-52": {"and": [{"$ref": "chain-51"}]}, "chain-53": {"and": [{"$ref": "chain-52"}]}, "chain-54": {"and": [{"$ref": "chain-53"}]}, "chain-55": {"and": [{"$ref": "chain-54"}]}, "chain-56": {"and": [{"$ref": "chain-55"}]}, "chain-57": {"and": [{"$ref": "chain-56"}]}, "chain-58": {"and": [{"$ref": "chain-57"}]}, "chain-59": {"and": [{"$ref": "chain-58"}]}, "chain-60": {"and": [{"$ref": "chain-59"}]}, "chain-61": {"and": [{"$ref": "chain-60"}]}, "chain-62": {"and": [{"$ref": "chain-61"}]}, "chain-63": {"and": [{"$ref": "chain-62"}]}, "chain-64": {"and": [{"$ref": "chain-63"}]}, "chain-65": {"and": [{"$ref": "chain-64"}]}, "chain-66": {"and": [{"$ref": "chain-65"}]}, "chain-67": {"and": [{"$ref": "chain-66"}]}, "chain-68": {"and": [{"$ref": "chain-67"}]}, "chain-69": {"and": [{"$ref": "chain-68"}]}, "chain-70": {"and": [{"$ref": "chain-69"}]}, "chain-71": {"and": [{"$ref": "chain-70"}]}, "chain-72": {"and": [{"$ref": "chain-71"}]}, "chain-73": {"and": [{"$ref": "chain-72"}]}, "chain-74": {"and": [{"$ref": "chain-73"}]}, "chain-75": {"and": [{"$ref": "chain-74"}]}, "chain-76": {"and": [{"$ref": "chain-75"}]}, "chain-77": {"and": [{"$ref": "chain-76"}]}, "chain-78": {"and": [{"$ref": "chain-77"}]}, "chain-79": {"and": [{"$ref": "chain-78"}]}, "chain-80": {"and": [{"$ref": "chain-79"}]}, "chain-81": {"and": [{"$ref": "chain-80"}]}, "chain-82": {"and": [{"$ref": "chain-81"}]}, "chain-83": {"and": [{"$ref": "chain-82"}]}, "chain-84": {"and": [{"$ref": "chain-83"}]}, "chain-85": {"and": [{"$ref": "chain-84"}]}, "chain-86": {"and": [{"$ref": "chain-85"}]}, "chain-87": {"and": [{"$ref": "chain-86"}]}, "chain-88": {"and": [{"$ref": "chain-87"}]}, "chain-89": {"and": [{"$ref": "chain-88"}]}, "chain-90": {"and": [{"$ref": "chain-89"}]}, "chain-91": {"and": [{"$ref": "chain-90"}]}, "chain-92": {"and": [{"$ref": "chain-91"}]}, "chain-93": {"and": [{"$ref": "chain-92"}]}, "chain-94": {"and": [{"$ref": "chain-93"}]}, "chain-95": {"and": [{"$ref": "chain-94"}]}, "chain-96": {"and": [{"$ref": "chain-95"}]}, "chain-97": {"and": [{"$ref": "chain-96"}]}, "chain-98": {"and": [{"$ref": "chain-97"}]}, "chain-99": {"and": [{"$ref": "chain-98"}]}, "chain-100": {"and": [{"$ref": "chain-99"}]}, "chain-101": {"and": [{"$ref": "chain-100"}]}, "chain-102": {"and": [{"$ref": "chain-101"}]}, "chain-103": {"and": [{"$ref": "chain-102"}]}, "chain-104": {"and": [{"$ref": "chain-103"}]}, "chain-105": {"and": [{"$ref": "chain-104"}]}, "chain-106": {"and": [{"$ref": "chain-105"}]}, "chain-107": {"and": [{"$ref": "chain-106"}]}, "chain-108": {"and": [{"$ref": "chain-107"}]}, "chain-109": {"and": [{"$ref": "chain-108"}]}, "chain-110": {"and": [{"$ref": "chain-109"}]}, "chain-111": {"and": [{"$ref": "chain-110"}]}, "chain-112": {"and": [{"$ref": "chain-111"}]}, "chain-113": {"and": [{"$ref": "chain-112"}]}, "chain-114": {"and": [{"$ref": "chain-113"}]}, "chain-115": {"and": [{"$ref": "chain-114"}]}, "chain-116": {"and": [{"$ref": "chain-115"}]}, "chain-117": {"and": [{"$ref": "chain-116"}]}, "chain-118": {"and": [{"$ref": "chain-117"}]}, "chain-119": {"and": [{"$ref": "chain-118"}]}, "chain-120": {"and": [{"$ref": "chain-119"}]}, "chain-121": {"and": [{"$ref": "chain-120"}]}, "chain-122": {"and": [{"$ref": "chain-121"}]}, "chain-123": {"and": [{"$ref": "chain-122"}]}, "chain-124": {"and": [{"$ref": "chain-123"}]}, "chain-125": {"and": [{"$ref": "chain-124"}]}, "chain-126": {"and": [{"$ref": "chain-125"}]}, "chain-127": {"and": [{"$ref": "chain-126"}]}, "chain-128": {"and": [{"$ref": "chain-127"}]}, "chain-129": {"and": [{"$ref": "chain-128"}]}, "chain-130": {"and": [{"$ref": "chain-129"}]}, "chain-131": {"and": [{"$ref": "chain-130"}]}, "chain-132": {"and": [{"$ref": "chain-131"}]}, "chain-133": {"and": [{"$ref": "chain-132"}]}, "chain-134": {"and": [{"$ref": "chain-133"}]}, "chain-135": {"and": [{"$ref": "chain-134"}]}, "chain-136": {"and": [{"$ref": "chain-135"}]}, "chain-137": {"and": [{"$ref": "chain-136"}]}, "chain-138": {"and": [{"$ref": "chain-137"}]}, "chain-139": {"and": [{"$ref": "chain-138"}]}, "chain-140": {"and": [{"$ref": "chain-139"}]}, "chain-141": {"and": [{"$ref": "chain-140"}]}, "chain-142": {"and": [{"$ref": "chain-141"}]}, "chain-143": {"and": [{"$ref": "chain-142"}]}, "chain-144": {"and": [{"$ref": "chain-143"}]}, "chain-145": {"and": [{"$ref": "chain-144"}]}, "chain-146": {"and": [{"$ref": "chain-145"}]}, "chain-147": {"and": [{"$ref": "chain-146"}]}, "chain-148": {"and": [{"$ref": "chain-147"}]}, "chain-149": {"and": [{"$ref": "chain-148"}]}, "chain-150": {"and": [{"$ref": "chain-149"}]}, "chain-151": {"and": [{"$ref": "chain-150"}]}, "chain-152": {"and": [{"$ref": "chain-151"}]}, "chain-153": {"and": [{"$ref": "chain-152"}]}, "chain-154": {"and": [{"$ref": "chain-153"}]}, "chain-155": {"and": [{"$ref": "chain-154"}]}, "chain-156": {"and": [{"$ref": "chain-155"}]}, "chain-157": {"and": [{"$ref": "chain-156"}]}, "chain-158": {"and": [{"$ref": "chain-157"}]}, "chain-159": {"and": [{"$ref": "chain-158"}]}, "chain-160": {"and": [{"$ref": "chain-159"}]}, "chain-161": {"and": [{"$ref": "chain-160"}]}, "chain-162": {"and": [{"$ref": "chain-161"}]}, "chain-163": {"and": [{"$ref": "chain-162"}]}, "chain-164": {"and": [{"$ref": "chain-163"}]}, "chain-165": {"and": [{"$ref": "chain-164"}]}, "chain-166": {"and": [{"$ref": "chain-165"}]}, "chain-167": {"and": [{"$ref": "chain-166"}]}, "chain-168": {"and": [{"$ref": "chain-167"}]}, "chain-169": {"and": [{"$ref": "chain-168"}]}, "chain-170": {"and": [{"$ref": "chain-169"}]}, "chain-171": {"and": [{"$ref": "chain-170"}]}, "chain-172": {"and": [{"$ref": "chain-171"}]}, "chain-173": {"and": [{"$ref": "chain-172"}]}, "chain-174": {"and": [{"$ref": "chain-173"}]}, "chain-175": {"and": [{"$ref": "chain-174"}]}, "chain-176": {"and": [{"$ref": "chain-175"}]}, "chain-177": {"and": [{"$ref": "chain-176"}]}, "chain-178": {"and": [{"$ref": "chain-177"}]}, "chain-179": {"and": [{"$ref": "chain-178"}]}, "chain-180": {"and": [{"$ref": "chain-179"}]}, "chain-181": {"and": [{"$ref": "chain-180"}]}, "chain-182": {"and": [{"$ref": "chain-181"}]}, "chain-183": {"and": [{"$ref": "chain-182"}]}, "chain-184": {"and": [{"$ref": "chain-183"}]}, "chain-185": {"and": [{"$ref": "chain-184"}]}, "chain-186": {"and": [{"$ref": "chain-185"}]}, "chain-187": {"and": [{"$ref": "chain-186"}]}, "chain-188": {"and": [{"$ref": "chain-187"}]}, "chain-189": {"and": [{"$ref": "chain-188"}]}, "chain-190": {"and": [{"$ref": "chain-189"}]}, "chain-191": {"and": [{"$ref": "chain-190"}]}, "chain-192": {"and": [{"$ref": "chain-191"}]}, "chain-193": {"and": [{"$ref": "chain-192"}]}, "chain-194": {"and": [{"$ref": "chain-193"}]}, "chain-195": {"and": [{"$ref": "chain-194"}]}, "chain-196": {"and": [{"$ref": "chain-195"}]}, "chain-197": {"and": [{"$ref": "chain-196"}]}, "chain-198": {"and": [{"$ref": "chain-197"}]}, "chain-199": {"and": [{"$ref": "chain-198"}]}, "chain-200": {"and": [{"$ref": "chain-199"}]}}}, "flag": "deep-chain-short", "type": "boolean", "default": false, "targetingKey": "u-35", "context": {"tier": "gold"}, "expect": {"value": true, "variant": "on", "reason": "TARGETING_MATCH"}},
+ {"name": "a chain of references nesting deeper than 128 levels is a parse error", "kind": "evaluate", "document": {"flags": {"fan-out-small": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "fan-5"}, "on", "off"]}}, "fan-out-huge": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "fan-20"}, "on", "off"]}}, "deep-chain-short": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "chain-20"}, "on", "off"]}}, "deep-chain-long": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "chain-200"}, "on", "off"]}}}, "$evaluators": {"fan-0": {"==": [{"var": "tier"}, "gold"]}, "fan-1": {"or": [{"$ref": "fan-0"}, {"$ref": "fan-0"}]}, "fan-2": {"or": [{"$ref": "fan-1"}, {"$ref": "fan-1"}]}, "fan-3": {"or": [{"$ref": "fan-2"}, {"$ref": "fan-2"}]}, "fan-4": {"or": [{"$ref": "fan-3"}, {"$ref": "fan-3"}]}, "fan-5": {"or": [{"$ref": "fan-4"}, {"$ref": "fan-4"}]}, "fan-6": {"or": [{"$ref": "fan-5"}, {"$ref": "fan-5"}]}, "fan-7": {"or": [{"$ref": "fan-6"}, {"$ref": "fan-6"}]}, "fan-8": {"or": [{"$ref": "fan-7"}, {"$ref": "fan-7"}]}, "fan-9": {"or": [{"$ref": "fan-8"}, {"$ref": "fan-8"}]}, "fan-10": {"or": [{"$ref": "fan-9"}, {"$ref": "fan-9"}]}, "fan-11": {"or": [{"$ref": "fan-10"}, {"$ref": "fan-10"}]}, "fan-12": {"or": [{"$ref": "fan-11"}, {"$ref": "fan-11"}]}, "fan-13": {"or": [{"$ref": "fan-12"}, {"$ref": "fan-12"}]}, "fan-14": {"or": [{"$ref": "fan-13"}, {"$ref": "fan-13"}]}, "fan-15": {"or": [{"$ref": "fan-14"}, {"$ref": "fan-14"}]}, "fan-16": {"or": [{"$ref": "fan-15"}, {"$ref": "fan-15"}]}, "fan-17": {"or": [{"$ref": "fan-16"}, {"$ref": "fan-16"}]}, "fan-18": {"or": [{"$ref": "fan-17"}, {"$ref": "fan-17"}]}, "fan-19": {"or": [{"$ref": "fan-18"}, {"$ref": "fan-18"}]}, "fan-20": {"or": [{"$ref": "fan-19"}, {"$ref": "fan-19"}]}, "chain-0": {"==": [{"var": "tier"}, "gold"]}, "chain-1": {"and": [{"$ref": "chain-0"}]}, "chain-2": {"and": [{"$ref": "chain-1"}]}, "chain-3": {"and": [{"$ref": "chain-2"}]}, "chain-4": {"and": [{"$ref": "chain-3"}]}, "chain-5": {"and": [{"$ref": "chain-4"}]}, "chain-6": {"and": [{"$ref": "chain-5"}]}, "chain-7": {"and": [{"$ref": "chain-6"}]}, "chain-8": {"and": [{"$ref": "chain-7"}]}, "chain-9": {"and": [{"$ref": "chain-8"}]}, "chain-10": {"and": [{"$ref": "chain-9"}]}, "chain-11": {"and": [{"$ref": "chain-10"}]}, "chain-12": {"and": [{"$ref": "chain-11"}]}, "chain-13": {"and": [{"$ref": "chain-12"}]}, "chain-14": {"and": [{"$ref": "chain-13"}]}, "chain-15": {"and": [{"$ref": "chain-14"}]}, "chain-16": {"and": [{"$ref": "chain-15"}]}, "chain-17": {"and": [{"$ref": "chain-16"}]}, "chain-18": {"and": [{"$ref": "chain-17"}]}, "chain-19": {"and": [{"$ref": "chain-18"}]}, "chain-20": {"and": [{"$ref": "chain-19"}]}, "chain-21": {"and": [{"$ref": "chain-20"}]}, "chain-22": {"and": [{"$ref": "chain-21"}]}, "chain-23": {"and": [{"$ref": "chain-22"}]}, "chain-24": {"and": [{"$ref": "chain-23"}]}, "chain-25": {"and": [{"$ref": "chain-24"}]}, "chain-26": {"and": [{"$ref": "chain-25"}]}, "chain-27": {"and": [{"$ref": "chain-26"}]}, "chain-28": {"and": [{"$ref": "chain-27"}]}, "chain-29": {"and": [{"$ref": "chain-28"}]}, "chain-30": {"and": [{"$ref": "chain-29"}]}, "chain-31": {"and": [{"$ref": "chain-30"}]}, "chain-32": {"and": [{"$ref": "chain-31"}]}, "chain-33": {"and": [{"$ref": "chain-32"}]}, "chain-34": {"and": [{"$ref": "chain-33"}]}, "chain-35": {"and": [{"$ref": "chain-34"}]}, "chain-36": {"and": [{"$ref": "chain-35"}]}, "chain-37": {"and": [{"$ref": "chain-36"}]}, "chain-38": {"and": [{"$ref": "chain-37"}]}, "chain-39": {"and": [{"$ref": "chain-38"}]}, "chain-40": {"and": [{"$ref": "chain-39"}]}, "chain-41": {"and": [{"$ref": "chain-40"}]}, "chain-42": {"and": [{"$ref": "chain-41"}]}, "chain-43": {"and": [{"$ref": "chain-42"}]}, "chain-44": {"and": [{"$ref": "chain-43"}]}, "chain-45": {"and": [{"$ref": "chain-44"}]}, "chain-46": {"and": [{"$ref": "chain-45"}]}, "chain-47": {"and": [{"$ref": "chain-46"}]}, "chain-48": {"and": [{"$ref": "chain-47"}]}, "chain-49": {"and": [{"$ref": "chain-48"}]}, "chain-50": {"and": [{"$ref": "chain-49"}]}, "chain-51": {"and": [{"$ref": "chain-50"}]}, "chain-52": {"and": [{"$ref": "chain-51"}]}, "chain-53": {"and": [{"$ref": "chain-52"}]}, "chain-54": {"and": [{"$ref": "chain-53"}]}, "chain-55": {"and": [{"$ref": "chain-54"}]}, "chain-56": {"and": [{"$ref": "chain-55"}]}, "chain-57": {"and": [{"$ref": "chain-56"}]}, "chain-58": {"and": [{"$ref": "chain-57"}]}, "chain-59": {"and": [{"$ref": "chain-58"}]}, "chain-60": {"and": [{"$ref": "chain-59"}]}, "chain-61": {"and": [{"$ref": "chain-60"}]}, "chain-62": {"and": [{"$ref": "chain-61"}]}, "chain-63": {"and": [{"$ref": "chain-62"}]}, "chain-64": {"and": [{"$ref": "chain-63"}]}, "chain-65": {"and": [{"$ref": "chain-64"}]}, "chain-66": {"and": [{"$ref": "chain-65"}]}, "chain-67": {"and": [{"$ref": "chain-66"}]}, "chain-68": {"and": [{"$ref": "chain-67"}]}, "chain-69": {"and": [{"$ref": "chain-68"}]}, "chain-70": {"and": [{"$ref": "chain-69"}]}, "chain-71": {"and": [{"$ref": "chain-70"}]}, "chain-72": {"and": [{"$ref": "chain-71"}]}, "chain-73": {"and": [{"$ref": "chain-72"}]}, "chain-74": {"and": [{"$ref": "chain-73"}]}, "chain-75": {"and": [{"$ref": "chain-74"}]}, "chain-76": {"and": [{"$ref": "chain-75"}]}, "chain-77": {"and": [{"$ref": "chain-76"}]}, "chain-78": {"and": [{"$ref": "chain-77"}]}, "chain-79": {"and": [{"$ref": "chain-78"}]}, "chain-80": {"and": [{"$ref": "chain-79"}]}, "chain-81": {"and": [{"$ref": "chain-80"}]}, "chain-82": {"and": [{"$ref": "chain-81"}]}, "chain-83": {"and": [{"$ref": "chain-82"}]}, "chain-84": {"and": [{"$ref": "chain-83"}]}, "chain-85": {"and": [{"$ref": "chain-84"}]}, "chain-86": {"and": [{"$ref": "chain-85"}]}, "chain-87": {"and": [{"$ref": "chain-86"}]}, "chain-88": {"and": [{"$ref": "chain-87"}]}, "chain-89": {"and": [{"$ref": "chain-88"}]}, "chain-90": {"and": [{"$ref": "chain-89"}]}, "chain-91": {"and": [{"$ref": "chain-90"}]}, "chain-92": {"and": [{"$ref": "chain-91"}]}, "chain-93": {"and": [{"$ref": "chain-92"}]}, "chain-94": {"and": [{"$ref": "chain-93"}]}, "chain-95": {"and": [{"$ref": "chain-94"}]}, "chain-96": {"and": [{"$ref": "chain-95"}]}, "chain-97": {"and": [{"$ref": "chain-96"}]}, "chain-98": {"and": [{"$ref": "chain-97"}]}, "chain-99": {"and": [{"$ref": "chain-98"}]}, "chain-100": {"and": [{"$ref": "chain-99"}]}, "chain-101": {"and": [{"$ref": "chain-100"}]}, "chain-102": {"and": [{"$ref": "chain-101"}]}, "chain-103": {"and": [{"$ref": "chain-102"}]}, "chain-104": {"and": [{"$ref": "chain-103"}]}, "chain-105": {"and": [{"$ref": "chain-104"}]}, "chain-106": {"and": [{"$ref": "chain-105"}]}, "chain-107": {"and": [{"$ref": "chain-106"}]}, "chain-108": {"and": [{"$ref": "chain-107"}]}, "chain-109": {"and": [{"$ref": "chain-108"}]}, "chain-110": {"and": [{"$ref": "chain-109"}]}, "chain-111": {"and": [{"$ref": "chain-110"}]}, "chain-112": {"and": [{"$ref": "chain-111"}]}, "chain-113": {"and": [{"$ref": "chain-112"}]}, "chain-114": {"and": [{"$ref": "chain-113"}]}, "chain-115": {"and": [{"$ref": "chain-114"}]}, "chain-116": {"and": [{"$ref": "chain-115"}]}, "chain-117": {"and": [{"$ref": "chain-116"}]}, "chain-118": {"and": [{"$ref": "chain-117"}]}, "chain-119": {"and": [{"$ref": "chain-118"}]}, "chain-120": {"and": [{"$ref": "chain-119"}]}, "chain-121": {"and": [{"$ref": "chain-120"}]}, "chain-122": {"and": [{"$ref": "chain-121"}]}, "chain-123": {"and": [{"$ref": "chain-122"}]}, "chain-124": {"and": [{"$ref": "chain-123"}]}, "chain-125": {"and": [{"$ref": "chain-124"}]}, "chain-126": {"and": [{"$ref": "chain-125"}]}, "chain-127": {"and": [{"$ref": "chain-126"}]}, "chain-128": {"and": [{"$ref": "chain-127"}]}, "chain-129": {"and": [{"$ref": "chain-128"}]}, "chain-130": {"and": [{"$ref": "chain-129"}]}, "chain-131": {"and": [{"$ref": "chain-130"}]}, "chain-132": {"and": [{"$ref": "chain-131"}]}, "chain-133": {"and": [{"$ref": "chain-132"}]}, "chain-134": {"and": [{"$ref": "chain-133"}]}, "chain-135": {"and": [{"$ref": "chain-134"}]}, "chain-136": {"and": [{"$ref": "chain-135"}]}, "chain-137": {"and": [{"$ref": "chain-136"}]}, "chain-138": {"and": [{"$ref": "chain-137"}]}, "chain-139": {"and": [{"$ref": "chain-138"}]}, "chain-140": {"and": [{"$ref": "chain-139"}]}, "chain-141": {"and": [{"$ref": "chain-140"}]}, "chain-142": {"and": [{"$ref": "chain-141"}]}, "chain-143": {"and": [{"$ref": "chain-142"}]}, "chain-144": {"and": [{"$ref": "chain-143"}]}, "chain-145": {"and": [{"$ref": "chain-144"}]}, "chain-146": {"and": [{"$ref": "chain-145"}]}, "chain-147": {"and": [{"$ref": "chain-146"}]}, "chain-148": {"and": [{"$ref": "chain-147"}]}, "chain-149": {"and": [{"$ref": "chain-148"}]}, "chain-150": {"and": [{"$ref": "chain-149"}]}, "chain-151": {"and": [{"$ref": "chain-150"}]}, "chain-152": {"and": [{"$ref": "chain-151"}]}, "chain-153": {"and": [{"$ref": "chain-152"}]}, "chain-154": {"and": [{"$ref": "chain-153"}]}, "chain-155": {"and": [{"$ref": "chain-154"}]}, "chain-156": {"and": [{"$ref": "chain-155"}]}, "chain-157": {"and": [{"$ref": "chain-156"}]}, "chain-158": {"and": [{"$ref": "chain-157"}]}, "chain-159": {"and": [{"$ref": "chain-158"}]}, "chain-160": {"and": [{"$ref": "chain-159"}]}, "chain-161": {"and": [{"$ref": "chain-160"}]}, "chain-162": {"and": [{"$ref": "chain-161"}]}, "chain-163": {"and": [{"$ref": "chain-162"}]}, "chain-164": {"and": [{"$ref": "chain-163"}]}, "chain-165": {"and": [{"$ref": "chain-164"}]}, "chain-166": {"and": [{"$ref": "chain-165"}]}, "chain-167": {"and": [{"$ref": "chain-166"}]}, "chain-168": {"and": [{"$ref": "chain-167"}]}, "chain-169": {"and": [{"$ref": "chain-168"}]}, "chain-170": {"and": [{"$ref": "chain-169"}]}, "chain-171": {"and": [{"$ref": "chain-170"}]}, "chain-172": {"and": [{"$ref": "chain-171"}]}, "chain-173": {"and": [{"$ref": "chain-172"}]}, "chain-174": {"and": [{"$ref": "chain-173"}]}, "chain-175": {"and": [{"$ref": "chain-174"}]}, "chain-176": {"and": [{"$ref": "chain-175"}]}, "chain-177": {"and": [{"$ref": "chain-176"}]}, "chain-178": {"and": [{"$ref": "chain-177"}]}, "chain-179": {"and": [{"$ref": "chain-178"}]}, "chain-180": {"and": [{"$ref": "chain-179"}]}, "chain-181": {"and": [{"$ref": "chain-180"}]}, "chain-182": {"and": [{"$ref": "chain-181"}]}, "chain-183": {"and": [{"$ref": "chain-182"}]}, "chain-184": {"and": [{"$ref": "chain-183"}]}, "chain-185": {"and": [{"$ref": "chain-184"}]}, "chain-186": {"and": [{"$ref": "chain-185"}]}, "chain-187": {"and": [{"$ref": "chain-186"}]}, "chain-188": {"and": [{"$ref": "chain-187"}]}, "chain-189": {"and": [{"$ref": "chain-188"}]}, "chain-190": {"and": [{"$ref": "chain-189"}]}, "chain-191": {"and": [{"$ref": "chain-190"}]}, "chain-192": {"and": [{"$ref": "chain-191"}]}, "chain-193": {"and": [{"$ref": "chain-192"}]}, "chain-194": {"and": [{"$ref": "chain-193"}]}, "chain-195": {"and": [{"$ref": "chain-194"}]}, "chain-196": {"and": [{"$ref": "chain-195"}]}, "chain-197": {"and": [{"$ref": "chain-196"}]}, "chain-198": {"and": [{"$ref": "chain-197"}]}, "chain-199": {"and": [{"$ref": "chain-198"}]}, "chain-200": {"and": [{"$ref": "chain-199"}]}}}, "flag": "deep-chain-long", "type": "boolean", "default": true, "targetingKey": "u-36", "context": {"tier": "gold"}, "expect": {"value": true, "variant": null, "reason": "ERROR", "errorCode": "PARSE_ERROR"}},
+ {"name": "references within the budget once each resolved reference costs one", "kind": "evaluate", "document": {"flags": {"ref-cost-under": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "alias-700"}, "on", "off"]}}, "ref-cost-over": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "alias-800"}, "on", "off"]}}}, "$evaluators": {"fan-0": {"==": [{"var": "tier"}, "gold"]}, "fan-1": {"or": [{"$ref": "fan-0"}, {"$ref": "fan-0"}]}, "fan-2": {"or": [{"$ref": "fan-1"}, {"$ref": "fan-1"}]}, "fan-3": {"or": [{"$ref": "fan-2"}, {"$ref": "fan-2"}]}, "fan-4": {"or": [{"$ref": "fan-3"}, {"$ref": "fan-3"}]}, "fan-5": {"or": [{"$ref": "fan-4"}, {"$ref": "fan-4"}]}, "fan-6": {"or": [{"$ref": "fan-5"}, {"$ref": "fan-5"}]}, "fan-7": {"or": [{"$ref": "fan-6"}, {"$ref": "fan-6"}]}, "fan-8": {"or": [{"$ref": "fan-7"}, {"$ref": "fan-7"}]}, "fan-9": {"or": [{"$ref": "fan-8"}, {"$ref": "fan-8"}]}, "fan-10": {"or": [{"$ref": "fan-9"}, {"$ref": "fan-9"}]}, "alias-0": {"$ref": "fan-10"}, "alias-1": {"$ref": "alias-0"}, "alias-2": {"$ref": "alias-1"}, "alias-3": {"$ref": "alias-2"}, "alias-4": {"$ref": "alias-3"}, "alias-5": {"$ref": "alias-4"}, "alias-6": {"$ref": "alias-5"}, "alias-7": {"$ref": "alias-6"}, "alias-8": {"$ref": "alias-7"}, "alias-9": {"$ref": "alias-8"}, "alias-10": {"$ref": "alias-9"}, "alias-11": {"$ref": "alias-10"}, "alias-12": {"$ref": "alias-11"}, "alias-13": {"$ref": "alias-12"}, "alias-14": {"$ref": "alias-13"}, "alias-15": {"$ref": "alias-14"}, "alias-16": {"$ref": "alias-15"}, "alias-17": {"$ref": "alias-16"}, "alias-18": {"$ref": "alias-17"}, "alias-19": {"$ref": "alias-18"}, "alias-20": {"$ref": "alias-19"}, "alias-21": {"$ref": "alias-20"}, "alias-22": {"$ref": "alias-21"}, "alias-23": {"$ref": "alias-22"}, "alias-24": {"$ref": "alias-23"}, "alias-25": {"$ref": "alias-24"}, "alias-26": {"$ref": "alias-25"}, "alias-27": {"$ref": "alias-26"}, "alias-28": {"$ref": "alias-27"}, "alias-29": {"$ref": "alias-28"}, "alias-30": {"$ref": "alias-29"}, "alias-31": {"$ref": "alias-30"}, "alias-32": {"$ref": "alias-31"}, "alias-33": {"$ref": "alias-32"}, "alias-34": {"$ref": "alias-33"}, "alias-35": {"$ref": "alias-34"}, "alias-36": {"$ref": "alias-35"}, "alias-37": {"$ref": "alias-36"}, "alias-38": {"$ref": "alias-37"}, "alias-39": {"$ref": "alias-38"}, "alias-40": {"$ref": "alias-39"}, "alias-41": {"$ref": "alias-40"}, "alias-42": {"$ref": "alias-41"}, "alias-43": {"$ref": "alias-42"}, "alias-44": {"$ref": "alias-43"}, "alias-45": {"$ref": "alias-44"}, "alias-46": {"$ref": "alias-45"}, "alias-47": {"$ref": "alias-46"}, "alias-48": {"$ref": "alias-47"}, "alias-49": {"$ref": "alias-48"}, "alias-50": {"$ref": "alias-49"}, "alias-51": {"$ref": "alias-50"}, "alias-52": {"$ref": "alias-51"}, "alias-53": {"$ref": "alias-52"}, "alias-54": {"$ref": "alias-53"}, "alias-55": {"$ref": "alias-54"}, "alias-56": {"$ref": "alias-55"}, "alias-57": {"$ref": "alias-56"}, "alias-58": {"$ref": "alias-57"}, "alias-59": {"$ref": "alias-58"}, "alias-60": {"$ref": "alias-59"}, "alias-61": {"$ref": "alias-60"}, "alias-62": {"$ref": "alias-61"}, "alias-63": {"$ref": "alias-62"}, "alias-64": {"$ref": "alias-63"}, "alias-65": {"$ref": "alias-64"}, "alias-66": {"$ref": "alias-65"}, "alias-67": {"$ref": "alias-66"}, "alias-68": {"$ref": "alias-67"}, "alias-69": {"$ref": "alias-68"}, "alias-70": {"$ref": "alias-69"}, "alias-71": {"$ref": "alias-70"}, "alias-72": {"$ref": "alias-71"}, "alias-73": {"$ref": "alias-72"}, "alias-74": {"$ref": "alias-73"}, "alias-75": {"$ref": "alias-74"}, "alias-76": {"$ref": "alias-75"}, "alias-77": {"$ref": "alias-76"}, "alias-78": {"$ref": "alias-77"}, "alias-79": {"$ref": "alias-78"}, "alias-80": {"$ref": "alias-79"}, "alias-81": {"$ref": "alias-80"}, "alias-82": {"$ref": "alias-81"}, "alias-83": {"$ref": "alias-82"}, "alias-84": {"$ref": "alias-83"}, "alias-85": {"$ref": "alias-84"}, "alias-86": {"$ref": "alias-85"}, "alias-87": {"$ref": "alias-86"}, "alias-88": {"$ref": "alias-87"}, "alias-89": {"$ref": "alias-88"}, "alias-90": {"$ref": "alias-89"}, "alias-91": {"$ref": "alias-90"}, "alias-92": {"$ref": "alias-91"}, "alias-93": {"$ref": "alias-92"}, "alias-94": {"$ref": "alias-93"}, "alias-95": {"$ref": "alias-94"}, "alias-96": {"$ref": "alias-95"}, "alias-97": {"$ref": "alias-96"}, "alias-98": {"$ref": "alias-97"}, "alias-99": {"$ref": "alias-98"}, "alias-100": {"$ref": "alias-99"}, "alias-101": {"$ref": "alias-100"}, "alias-102": {"$ref": "alias-101"}, "alias-103": {"$ref": "alias-102"}, "alias-104": {"$ref": "alias-103"}, "alias-105": {"$ref": "alias-104"}, "alias-106": {"$ref": "alias-105"}, "alias-107": {"$ref": "alias-106"}, "alias-108": {"$ref": "alias-107"}, "alias-109": {"$ref": "alias-108"}, "alias-110": {"$ref": "alias-109"}, "alias-111": {"$ref": "alias-110"}, "alias-112": {"$ref": "alias-111"}, "alias-113": {"$ref": "alias-112"}, "alias-114": {"$ref": "alias-113"}, "alias-115": {"$ref": "alias-114"}, "alias-116": {"$ref": "alias-115"}, "alias-117": {"$ref": "alias-116"}, "alias-118": {"$ref": "alias-117"}, "alias-119": {"$ref": "alias-118"}, "alias-120": {"$ref": "alias-119"}, "alias-121": {"$ref": "alias-120"}, "alias-122": {"$ref": "alias-121"}, "alias-123": {"$ref": "alias-122"}, "alias-124": {"$ref": "alias-123"}, "alias-125": {"$ref": "alias-124"}, "alias-126": {"$ref": "alias-125"}, "alias-127": {"$ref": "alias-126"}, "alias-128": {"$ref": "alias-127"}, "alias-129": {"$ref": "alias-128"}, "alias-130": {"$ref": "alias-129"}, "alias-131": {"$ref": "alias-130"}, "alias-132": {"$ref": "alias-131"}, "alias-133": {"$ref": "alias-132"}, "alias-134": {"$ref": "alias-133"}, "alias-135": {"$ref": "alias-134"}, "alias-136": {"$ref": "alias-135"}, "alias-137": {"$ref": "alias-136"}, "alias-138": {"$ref": "alias-137"}, "alias-139": {"$ref": "alias-138"}, "alias-140": {"$ref": "alias-139"}, "alias-141": {"$ref": "alias-140"}, "alias-142": {"$ref": "alias-141"}, "alias-143": {"$ref": "alias-142"}, "alias-144": {"$ref": "alias-143"}, "alias-145": {"$ref": "alias-144"}, "alias-146": {"$ref": "alias-145"}, "alias-147": {"$ref": "alias-146"}, "alias-148": {"$ref": "alias-147"}, "alias-149": {"$ref": "alias-148"}, "alias-150": {"$ref": "alias-149"}, "alias-151": {"$ref": "alias-150"}, "alias-152": {"$ref": "alias-151"}, "alias-153": {"$ref": "alias-152"}, "alias-154": {"$ref": "alias-153"}, "alias-155": {"$ref": "alias-154"}, "alias-156": {"$ref": "alias-155"}, "alias-157": {"$ref": "alias-156"}, "alias-158": {"$ref": "alias-157"}, "alias-159": {"$ref": "alias-158"}, "alias-160": {"$ref": "alias-159"}, "alias-161": {"$ref": "alias-160"}, "alias-162": {"$ref": "alias-161"}, "alias-163": {"$ref": "alias-162"}, "alias-164": {"$ref": "alias-163"}, "alias-165": {"$ref": "alias-164"}, "alias-166": {"$ref": "alias-165"}, "alias-167": {"$ref": "alias-166"}, "alias-168": {"$ref": "alias-167"}, "alias-169": {"$ref": "alias-168"}, "alias-170": {"$ref": "alias-169"}, "alias-171": {"$ref": "alias-170"}, "alias-172": {"$ref": "alias-171"}, "alias-173": {"$ref": "alias-172"}, "alias-174": {"$ref": "alias-173"}, "alias-175": {"$ref": "alias-174"}, "alias-176": {"$ref": "alias-175"}, "alias-177": {"$ref": "alias-176"}, "alias-178": {"$ref": "alias-177"}, "alias-179": {"$ref": "alias-178"}, "alias-180": {"$ref": "alias-179"}, "alias-181": {"$ref": "alias-180"}, "alias-182": {"$ref": "alias-181"}, "alias-183": {"$ref": "alias-182"}, "alias-184": {"$ref": "alias-183"}, "alias-185": {"$ref": "alias-184"}, "alias-186": {"$ref": "alias-185"}, "alias-187": {"$ref": "alias-186"}, "alias-188": {"$ref": "alias-187"}, "alias-189": {"$ref": "alias-188"}, "alias-190": {"$ref": "alias-189"}, "alias-191": {"$ref": "alias-190"}, "alias-192": {"$ref": "alias-191"}, "alias-193": {"$ref": "alias-192"}, "alias-194": {"$ref": "alias-193"}, "alias-195": {"$ref": "alias-194"}, "alias-196": {"$ref": "alias-195"}, "alias-197": {"$ref": "alias-196"}, "alias-198": {"$ref": "alias-197"}, "alias-199": {"$ref": "alias-198"}, "alias-200": {"$ref": "alias-199"}, "alias-201": {"$ref": "alias-200"}, "alias-202": {"$ref": "alias-201"}, "alias-203": {"$ref": "alias-202"}, "alias-204": {"$ref": "alias-203"}, "alias-205": {"$ref": "alias-204"}, "alias-206": {"$ref": "alias-205"}, "alias-207": {"$ref": "alias-206"}, "alias-208": {"$ref": "alias-207"}, "alias-209": {"$ref": "alias-208"}, "alias-210": {"$ref": "alias-209"}, "alias-211": {"$ref": "alias-210"}, "alias-212": {"$ref": "alias-211"}, "alias-213": {"$ref": "alias-212"}, "alias-214": {"$ref": "alias-213"}, "alias-215": {"$ref": "alias-214"}, "alias-216": {"$ref": "alias-215"}, "alias-217": {"$ref": "alias-216"}, "alias-218": {"$ref": "alias-217"}, "alias-219": {"$ref": "alias-218"}, "alias-220": {"$ref": "alias-219"}, "alias-221": {"$ref": "alias-220"}, "alias-222": {"$ref": "alias-221"}, "alias-223": {"$ref": "alias-222"}, "alias-224": {"$ref": "alias-223"}, "alias-225": {"$ref": "alias-224"}, "alias-226": {"$ref": "alias-225"}, "alias-227": {"$ref": "alias-226"}, "alias-228": {"$ref": "alias-227"}, "alias-229": {"$ref": "alias-228"}, "alias-230": {"$ref": "alias-229"}, "alias-231": {"$ref": "alias-230"}, "alias-232": {"$ref": "alias-231"}, "alias-233": {"$ref": "alias-232"}, "alias-234": {"$ref": "alias-233"}, "alias-235": {"$ref": "alias-234"}, "alias-236": {"$ref": "alias-235"}, "alias-237": {"$ref": "alias-236"}, "alias-238": {"$ref": "alias-237"}, "alias-239": {"$ref": "alias-238"}, "alias-240": {"$ref": "alias-239"}, "alias-241": {"$ref": "alias-240"}, "alias-242": {"$ref": "alias-241"}, "alias-243": {"$ref": "alias-242"}, "alias-244": {"$ref": "alias-243"}, "alias-245": {"$ref": "alias-244"}, "alias-246": {"$ref": "alias-245"}, "alias-247": {"$ref": "alias-246"}, "alias-248": {"$ref": "alias-247"}, "alias-249": {"$ref": "alias-248"}, "alias-250": {"$ref": "alias-249"}, "alias-251": {"$ref": "alias-250"}, "alias-252": {"$ref": "alias-251"}, "alias-253": {"$ref": "alias-252"}, "alias-254": {"$ref": "alias-253"}, "alias-255": {"$ref": "alias-254"}, "alias-256": {"$ref": "alias-255"}, "alias-257": {"$ref": "alias-256"}, "alias-258": {"$ref": "alias-257"}, "alias-259": {"$ref": "alias-258"}, "alias-260": {"$ref": "alias-259"}, "alias-261": {"$ref": "alias-260"}, "alias-262": {"$ref": "alias-261"}, "alias-263": {"$ref": "alias-262"}, "alias-264": {"$ref": "alias-263"}, "alias-265": {"$ref": "alias-264"}, "alias-266": {"$ref": "alias-265"}, "alias-267": {"$ref": "alias-266"}, "alias-268": {"$ref": "alias-267"}, "alias-269": {"$ref": "alias-268"}, "alias-270": {"$ref": "alias-269"}, "alias-271": {"$ref": "alias-270"}, "alias-272": {"$ref": "alias-271"}, "alias-273": {"$ref": "alias-272"}, "alias-274": {"$ref": "alias-273"}, "alias-275": {"$ref": "alias-274"}, "alias-276": {"$ref": "alias-275"}, "alias-277": {"$ref": "alias-276"}, "alias-278": {"$ref": "alias-277"}, "alias-279": {"$ref": "alias-278"}, "alias-280": {"$ref": "alias-279"}, "alias-281": {"$ref": "alias-280"}, "alias-282": {"$ref": "alias-281"}, "alias-283": {"$ref": "alias-282"}, "alias-284": {"$ref": "alias-283"}, "alias-285": {"$ref": "alias-284"}, "alias-286": {"$ref": "alias-285"}, "alias-287": {"$ref": "alias-286"}, "alias-288": {"$ref": "alias-287"}, "alias-289": {"$ref": "alias-288"}, "alias-290": {"$ref": "alias-289"}, "alias-291": {"$ref": "alias-290"}, "alias-292": {"$ref": "alias-291"}, "alias-293": {"$ref": "alias-292"}, "alias-294": {"$ref": "alias-293"}, "alias-295": {"$ref": "alias-294"}, "alias-296": {"$ref": "alias-295"}, "alias-297": {"$ref": "alias-296"}, "alias-298": {"$ref": "alias-297"}, "alias-299": {"$ref": "alias-298"}, "alias-300": {"$ref": "alias-299"}, "alias-301": {"$ref": "alias-300"}, "alias-302": {"$ref": "alias-301"}, "alias-303": {"$ref": "alias-302"}, "alias-304": {"$ref": "alias-303"}, "alias-305": {"$ref": "alias-304"}, "alias-306": {"$ref": "alias-305"}, "alias-307": {"$ref": "alias-306"}, "alias-308": {"$ref": "alias-307"}, "alias-309": {"$ref": "alias-308"}, "alias-310": {"$ref": "alias-309"}, "alias-311": {"$ref": "alias-310"}, "alias-312": {"$ref": "alias-311"}, "alias-313": {"$ref": "alias-312"}, "alias-314": {"$ref": "alias-313"}, "alias-315": {"$ref": "alias-314"}, "alias-316": {"$ref": "alias-315"}, "alias-317": {"$ref": "alias-316"}, "alias-318": {"$ref": "alias-317"}, "alias-319": {"$ref": "alias-318"}, "alias-320": {"$ref": "alias-319"}, "alias-321": {"$ref": "alias-320"}, "alias-322": {"$ref": "alias-321"}, "alias-323": {"$ref": "alias-322"}, "alias-324": {"$ref": "alias-323"}, "alias-325": {"$ref": "alias-324"}, "alias-326": {"$ref": "alias-325"}, "alias-327": {"$ref": "alias-326"}, "alias-328": {"$ref": "alias-327"}, "alias-329": {"$ref": "alias-328"}, "alias-330": {"$ref": "alias-329"}, "alias-331": {"$ref": "alias-330"}, "alias-332": {"$ref": "alias-331"}, "alias-333": {"$ref": "alias-332"}, "alias-334": {"$ref": "alias-333"}, "alias-335": {"$ref": "alias-334"}, "alias-336": {"$ref": "alias-335"}, "alias-337": {"$ref": "alias-336"}, "alias-338": {"$ref": "alias-337"}, "alias-339": {"$ref": "alias-338"}, "alias-340": {"$ref": "alias-339"}, "alias-341": {"$ref": "alias-340"}, "alias-342": {"$ref": "alias-341"}, "alias-343": {"$ref": "alias-342"}, "alias-344": {"$ref": "alias-343"}, "alias-345": {"$ref": "alias-344"}, "alias-346": {"$ref": "alias-345"}, "alias-347": {"$ref": "alias-346"}, "alias-348": {"$ref": "alias-347"}, "alias-349": {"$ref": "alias-348"}, "alias-350": {"$ref": "alias-349"}, "alias-351": {"$ref": "alias-350"}, "alias-352": {"$ref": "alias-351"}, "alias-353": {"$ref": "alias-352"}, "alias-354": {"$ref": "alias-353"}, "alias-355": {"$ref": "alias-354"}, "alias-356": {"$ref": "alias-355"}, "alias-357": {"$ref": "alias-356"}, "alias-358": {"$ref": "alias-357"}, "alias-359": {"$ref": "alias-358"}, "alias-360": {"$ref": "alias-359"}, "alias-361": {"$ref": "alias-360"}, "alias-362": {"$ref": "alias-361"}, "alias-363": {"$ref": "alias-362"}, "alias-364": {"$ref": "alias-363"}, "alias-365": {"$ref": "alias-364"}, "alias-366": {"$ref": "alias-365"}, "alias-367": {"$ref": "alias-366"}, "alias-368": {"$ref": "alias-367"}, "alias-369": {"$ref": "alias-368"}, "alias-370": {"$ref": "alias-369"}, "alias-371": {"$ref": "alias-370"}, "alias-372": {"$ref": "alias-371"}, "alias-373": {"$ref": "alias-372"}, "alias-374": {"$ref": "alias-373"}, "alias-375": {"$ref": "alias-374"}, "alias-376": {"$ref": "alias-375"}, "alias-377": {"$ref": "alias-376"}, "alias-378": {"$ref": "alias-377"}, "alias-379": {"$ref": "alias-378"}, "alias-380": {"$ref": "alias-379"}, "alias-381": {"$ref": "alias-380"}, "alias-382": {"$ref": "alias-381"}, "alias-383": {"$ref": "alias-382"}, "alias-384": {"$ref": "alias-383"}, "alias-385": {"$ref": "alias-384"}, "alias-386": {"$ref": "alias-385"}, "alias-387": {"$ref": "alias-386"}, "alias-388": {"$ref": "alias-387"}, "alias-389": {"$ref": "alias-388"}, "alias-390": {"$ref": "alias-389"}, "alias-391": {"$ref": "alias-390"}, "alias-392": {"$ref": "alias-391"}, "alias-393": {"$ref": "alias-392"}, "alias-394": {"$ref": "alias-393"}, "alias-395": {"$ref": "alias-394"}, "alias-396": {"$ref": "alias-395"}, "alias-397": {"$ref": "alias-396"}, "alias-398": {"$ref": "alias-397"}, "alias-399": {"$ref": "alias-398"}, "alias-400": {"$ref": "alias-399"}, "alias-401": {"$ref": "alias-400"}, "alias-402": {"$ref": "alias-401"}, "alias-403": {"$ref": "alias-402"}, "alias-404": {"$ref": "alias-403"}, "alias-405": {"$ref": "alias-404"}, "alias-406": {"$ref": "alias-405"}, "alias-407": {"$ref": "alias-406"}, "alias-408": {"$ref": "alias-407"}, "alias-409": {"$ref": "alias-408"}, "alias-410": {"$ref": "alias-409"}, "alias-411": {"$ref": "alias-410"}, "alias-412": {"$ref": "alias-411"}, "alias-413": {"$ref": "alias-412"}, "alias-414": {"$ref": "alias-413"}, "alias-415": {"$ref": "alias-414"}, "alias-416": {"$ref": "alias-415"}, "alias-417": {"$ref": "alias-416"}, "alias-418": {"$ref": "alias-417"}, "alias-419": {"$ref": "alias-418"}, "alias-420": {"$ref": "alias-419"}, "alias-421": {"$ref": "alias-420"}, "alias-422": {"$ref": "alias-421"}, "alias-423": {"$ref": "alias-422"}, "alias-424": {"$ref": "alias-423"}, "alias-425": {"$ref": "alias-424"}, "alias-426": {"$ref": "alias-425"}, "alias-427": {"$ref": "alias-426"}, "alias-428": {"$ref": "alias-427"}, "alias-429": {"$ref": "alias-428"}, "alias-430": {"$ref": "alias-429"}, "alias-431": {"$ref": "alias-430"}, "alias-432": {"$ref": "alias-431"}, "alias-433": {"$ref": "alias-432"}, "alias-434": {"$ref": "alias-433"}, "alias-435": {"$ref": "alias-434"}, "alias-436": {"$ref": "alias-435"}, "alias-437": {"$ref": "alias-436"}, "alias-438": {"$ref": "alias-437"}, "alias-439": {"$ref": "alias-438"}, "alias-440": {"$ref": "alias-439"}, "alias-441": {"$ref": "alias-440"}, "alias-442": {"$ref": "alias-441"}, "alias-443": {"$ref": "alias-442"}, "alias-444": {"$ref": "alias-443"}, "alias-445": {"$ref": "alias-444"}, "alias-446": {"$ref": "alias-445"}, "alias-447": {"$ref": "alias-446"}, "alias-448": {"$ref": "alias-447"}, "alias-449": {"$ref": "alias-448"}, "alias-450": {"$ref": "alias-449"}, "alias-451": {"$ref": "alias-450"}, "alias-452": {"$ref": "alias-451"}, "alias-453": {"$ref": "alias-452"}, "alias-454": {"$ref": "alias-453"}, "alias-455": {"$ref": "alias-454"}, "alias-456": {"$ref": "alias-455"}, "alias-457": {"$ref": "alias-456"}, "alias-458": {"$ref": "alias-457"}, "alias-459": {"$ref": "alias-458"}, "alias-460": {"$ref": "alias-459"}, "alias-461": {"$ref": "alias-460"}, "alias-462": {"$ref": "alias-461"}, "alias-463": {"$ref": "alias-462"}, "alias-464": {"$ref": "alias-463"}, "alias-465": {"$ref": "alias-464"}, "alias-466": {"$ref": "alias-465"}, "alias-467": {"$ref": "alias-466"}, "alias-468": {"$ref": "alias-467"}, "alias-469": {"$ref": "alias-468"}, "alias-470": {"$ref": "alias-469"}, "alias-471": {"$ref": "alias-470"}, "alias-472": {"$ref": "alias-471"}, "alias-473": {"$ref": "alias-472"}, "alias-474": {"$ref": "alias-473"}, "alias-475": {"$ref": "alias-474"}, "alias-476": {"$ref": "alias-475"}, "alias-477": {"$ref": "alias-476"}, "alias-478": {"$ref": "alias-477"}, "alias-479": {"$ref": "alias-478"}, "alias-480": {"$ref": "alias-479"}, "alias-481": {"$ref": "alias-480"}, "alias-482": {"$ref": "alias-481"}, "alias-483": {"$ref": "alias-482"}, "alias-484": {"$ref": "alias-483"}, "alias-485": {"$ref": "alias-484"}, "alias-486": {"$ref": "alias-485"}, "alias-487": {"$ref": "alias-486"}, "alias-488": {"$ref": "alias-487"}, "alias-489": {"$ref": "alias-488"}, "alias-490": {"$ref": "alias-489"}, "alias-491": {"$ref": "alias-490"}, "alias-492": {"$ref": "alias-491"}, "alias-493": {"$ref": "alias-492"}, "alias-494": {"$ref": "alias-493"}, "alias-495": {"$ref": "alias-494"}, "alias-496": {"$ref": "alias-495"}, "alias-497": {"$ref": "alias-496"}, "alias-498": {"$ref": "alias-497"}, "alias-499": {"$ref": "alias-498"}, "alias-500": {"$ref": "alias-499"}, "alias-501": {"$ref": "alias-500"}, "alias-502": {"$ref": "alias-501"}, "alias-503": {"$ref": "alias-502"}, "alias-504": {"$ref": "alias-503"}, "alias-505": {"$ref": "alias-504"}, "alias-506": {"$ref": "alias-505"}, "alias-507": {"$ref": "alias-506"}, "alias-508": {"$ref": "alias-507"}, "alias-509": {"$ref": "alias-508"}, "alias-510": {"$ref": "alias-509"}, "alias-511": {"$ref": "alias-510"}, "alias-512": {"$ref": "alias-511"}, "alias-513": {"$ref": "alias-512"}, "alias-514": {"$ref": "alias-513"}, "alias-515": {"$ref": "alias-514"}, "alias-516": {"$ref": "alias-515"}, "alias-517": {"$ref": "alias-516"}, "alias-518": {"$ref": "alias-517"}, "alias-519": {"$ref": "alias-518"}, "alias-520": {"$ref": "alias-519"}, "alias-521": {"$ref": "alias-520"}, "alias-522": {"$ref": "alias-521"}, "alias-523": {"$ref": "alias-522"}, "alias-524": {"$ref": "alias-523"}, "alias-525": {"$ref": "alias-524"}, "alias-526": {"$ref": "alias-525"}, "alias-527": {"$ref": "alias-526"}, "alias-528": {"$ref": "alias-527"}, "alias-529": {"$ref": "alias-528"}, "alias-530": {"$ref": "alias-529"}, "alias-531": {"$ref": "alias-530"}, "alias-532": {"$ref": "alias-531"}, "alias-533": {"$ref": "alias-532"}, "alias-534": {"$ref": "alias-533"}, "alias-535": {"$ref": "alias-534"}, "alias-536": {"$ref": "alias-535"}, "alias-537": {"$ref": "alias-536"}, "alias-538": {"$ref": "alias-537"}, "alias-539": {"$ref": "alias-538"}, "alias-540": {"$ref": "alias-539"}, "alias-541": {"$ref": "alias-540"}, "alias-542": {"$ref": "alias-541"}, "alias-543": {"$ref": "alias-542"}, "alias-544": {"$ref": "alias-543"}, "alias-545": {"$ref": "alias-544"}, "alias-546": {"$ref": "alias-545"}, "alias-547": {"$ref": "alias-546"}, "alias-548": {"$ref": "alias-547"}, "alias-549": {"$ref": "alias-548"}, "alias-550": {"$ref": "alias-549"}, "alias-551": {"$ref": "alias-550"}, "alias-552": {"$ref": "alias-551"}, "alias-553": {"$ref": "alias-552"}, "alias-554": {"$ref": "alias-553"}, "alias-555": {"$ref": "alias-554"}, "alias-556": {"$ref": "alias-555"}, "alias-557": {"$ref": "alias-556"}, "alias-558": {"$ref": "alias-557"}, "alias-559": {"$ref": "alias-558"}, "alias-560": {"$ref": "alias-559"}, "alias-561": {"$ref": "alias-560"}, "alias-562": {"$ref": "alias-561"}, "alias-563": {"$ref": "alias-562"}, "alias-564": {"$ref": "alias-563"}, "alias-565": {"$ref": "alias-564"}, "alias-566": {"$ref": "alias-565"}, "alias-567": {"$ref": "alias-566"}, "alias-568": {"$ref": "alias-567"}, "alias-569": {"$ref": "alias-568"}, "alias-570": {"$ref": "alias-569"}, "alias-571": {"$ref": "alias-570"}, "alias-572": {"$ref": "alias-571"}, "alias-573": {"$ref": "alias-572"}, "alias-574": {"$ref": "alias-573"}, "alias-575": {"$ref": "alias-574"}, "alias-576": {"$ref": "alias-575"}, "alias-577": {"$ref": "alias-576"}, "alias-578": {"$ref": "alias-577"}, "alias-579": {"$ref": "alias-578"}, "alias-580": {"$ref": "alias-579"}, "alias-581": {"$ref": "alias-580"}, "alias-582": {"$ref": "alias-581"}, "alias-583": {"$ref": "alias-582"}, "alias-584": {"$ref": "alias-583"}, "alias-585": {"$ref": "alias-584"}, "alias-586": {"$ref": "alias-585"}, "alias-587": {"$ref": "alias-586"}, "alias-588": {"$ref": "alias-587"}, "alias-589": {"$ref": "alias-588"}, "alias-590": {"$ref": "alias-589"}, "alias-591": {"$ref": "alias-590"}, "alias-592": {"$ref": "alias-591"}, "alias-593": {"$ref": "alias-592"}, "alias-594": {"$ref": "alias-593"}, "alias-595": {"$ref": "alias-594"}, "alias-596": {"$ref": "alias-595"}, "alias-597": {"$ref": "alias-596"}, "alias-598": {"$ref": "alias-597"}, "alias-599": {"$ref": "alias-598"}, "alias-600": {"$ref": "alias-599"}, "alias-601": {"$ref": "alias-600"}, "alias-602": {"$ref": "alias-601"}, "alias-603": {"$ref": "alias-602"}, "alias-604": {"$ref": "alias-603"}, "alias-605": {"$ref": "alias-604"}, "alias-606": {"$ref": "alias-605"}, "alias-607": {"$ref": "alias-606"}, "alias-608": {"$ref": "alias-607"}, "alias-609": {"$ref": "alias-608"}, "alias-610": {"$ref": "alias-609"}, "alias-611": {"$ref": "alias-610"}, "alias-612": {"$ref": "alias-611"}, "alias-613": {"$ref": "alias-612"}, "alias-614": {"$ref": "alias-613"}, "alias-615": {"$ref": "alias-614"}, "alias-616": {"$ref": "alias-615"}, "alias-617": {"$ref": "alias-616"}, "alias-618": {"$ref": "alias-617"}, "alias-619": {"$ref": "alias-618"}, "alias-620": {"$ref": "alias-619"}, "alias-621": {"$ref": "alias-620"}, "alias-622": {"$ref": "alias-621"}, "alias-623": {"$ref": "alias-622"}, "alias-624": {"$ref": "alias-623"}, "alias-625": {"$ref": "alias-624"}, "alias-626": {"$ref": "alias-625"}, "alias-627": {"$ref": "alias-626"}, "alias-628": {"$ref": "alias-627"}, "alias-629": {"$ref": "alias-628"}, "alias-630": {"$ref": "alias-629"}, "alias-631": {"$ref": "alias-630"}, "alias-632": {"$ref": "alias-631"}, "alias-633": {"$ref": "alias-632"}, "alias-634": {"$ref": "alias-633"}, "alias-635": {"$ref": "alias-634"}, "alias-636": {"$ref": "alias-635"}, "alias-637": {"$ref": "alias-636"}, "alias-638": {"$ref": "alias-637"}, "alias-639": {"$ref": "alias-638"}, "alias-640": {"$ref": "alias-639"}, "alias-641": {"$ref": "alias-640"}, "alias-642": {"$ref": "alias-641"}, "alias-643": {"$ref": "alias-642"}, "alias-644": {"$ref": "alias-643"}, "alias-645": {"$ref": "alias-644"}, "alias-646": {"$ref": "alias-645"}, "alias-647": {"$ref": "alias-646"}, "alias-648": {"$ref": "alias-647"}, "alias-649": {"$ref": "alias-648"}, "alias-650": {"$ref": "alias-649"}, "alias-651": {"$ref": "alias-650"}, "alias-652": {"$ref": "alias-651"}, "alias-653": {"$ref": "alias-652"}, "alias-654": {"$ref": "alias-653"}, "alias-655": {"$ref": "alias-654"}, "alias-656": {"$ref": "alias-655"}, "alias-657": {"$ref": "alias-656"}, "alias-658": {"$ref": "alias-657"}, "alias-659": {"$ref": "alias-658"}, "alias-660": {"$ref": "alias-659"}, "alias-661": {"$ref": "alias-660"}, "alias-662": {"$ref": "alias-661"}, "alias-663": {"$ref": "alias-662"}, "alias-664": {"$ref": "alias-663"}, "alias-665": {"$ref": "alias-664"}, "alias-666": {"$ref": "alias-665"}, "alias-667": {"$ref": "alias-666"}, "alias-668": {"$ref": "alias-667"}, "alias-669": {"$ref": "alias-668"}, "alias-670": {"$ref": "alias-669"}, "alias-671": {"$ref": "alias-670"}, "alias-672": {"$ref": "alias-671"}, "alias-673": {"$ref": "alias-672"}, "alias-674": {"$ref": "alias-673"}, "alias-675": {"$ref": "alias-674"}, "alias-676": {"$ref": "alias-675"}, "alias-677": {"$ref": "alias-676"}, "alias-678": {"$ref": "alias-677"}, "alias-679": {"$ref": "alias-678"}, "alias-680": {"$ref": "alias-679"}, "alias-681": {"$ref": "alias-680"}, "alias-682": {"$ref": "alias-681"}, "alias-683": {"$ref": "alias-682"}, "alias-684": {"$ref": "alias-683"}, "alias-685": {"$ref": "alias-684"}, "alias-686": {"$ref": "alias-685"}, "alias-687": {"$ref": "alias-686"}, "alias-688": {"$ref": "alias-687"}, "alias-689": {"$ref": "alias-688"}, "alias-690": {"$ref": "alias-689"}, "alias-691": {"$ref": "alias-690"}, "alias-692": {"$ref": "alias-691"}, "alias-693": {"$ref": "alias-692"}, "alias-694": {"$ref": "alias-693"}, "alias-695": {"$ref": "alias-694"}, "alias-696": {"$ref": "alias-695"}, "alias-697": {"$ref": "alias-696"}, "alias-698": {"$ref": "alias-697"}, "alias-699": {"$ref": "alias-698"}, "alias-700": {"$ref": "alias-699"}, "alias-701": {"$ref": "alias-700"}, "alias-702": {"$ref": "alias-701"}, "alias-703": {"$ref": "alias-702"}, "alias-704": {"$ref": "alias-703"}, "alias-705": {"$ref": "alias-704"}, "alias-706": {"$ref": "alias-705"}, "alias-707": {"$ref": "alias-706"}, "alias-708": {"$ref": "alias-707"}, "alias-709": {"$ref": "alias-708"}, "alias-710": {"$ref": "alias-709"}, "alias-711": {"$ref": "alias-710"}, "alias-712": {"$ref": "alias-711"}, "alias-713": {"$ref": "alias-712"}, "alias-714": {"$ref": "alias-713"}, "alias-715": {"$ref": "alias-714"}, "alias-716": {"$ref": "alias-715"}, "alias-717": {"$ref": "alias-716"}, "alias-718": {"$ref": "alias-717"}, "alias-719": {"$ref": "alias-718"}, "alias-720": {"$ref": "alias-719"}, "alias-721": {"$ref": "alias-720"}, "alias-722": {"$ref": "alias-721"}, "alias-723": {"$ref": "alias-722"}, "alias-724": {"$ref": "alias-723"}, "alias-725": {"$ref": "alias-724"}, "alias-726": {"$ref": "alias-725"}, "alias-727": {"$ref": "alias-726"}, "alias-728": {"$ref": "alias-727"}, "alias-729": {"$ref": "alias-728"}, "alias-730": {"$ref": "alias-729"}, "alias-731": {"$ref": "alias-730"}, "alias-732": {"$ref": "alias-731"}, "alias-733": {"$ref": "alias-732"}, "alias-734": {"$ref": "alias-733"}, "alias-735": {"$ref": "alias-734"}, "alias-736": {"$ref": "alias-735"}, "alias-737": {"$ref": "alias-736"}, "alias-738": {"$ref": "alias-737"}, "alias-739": {"$ref": "alias-738"}, "alias-740": {"$ref": "alias-739"}, "alias-741": {"$ref": "alias-740"}, "alias-742": {"$ref": "alias-741"}, "alias-743": {"$ref": "alias-742"}, "alias-744": {"$ref": "alias-743"}, "alias-745": {"$ref": "alias-744"}, "alias-746": {"$ref": "alias-745"}, "alias-747": {"$ref": "alias-746"}, "alias-748": {"$ref": "alias-747"}, "alias-749": {"$ref": "alias-748"}, "alias-750": {"$ref": "alias-749"}, "alias-751": {"$ref": "alias-750"}, "alias-752": {"$ref": "alias-751"}, "alias-753": {"$ref": "alias-752"}, "alias-754": {"$ref": "alias-753"}, "alias-755": {"$ref": "alias-754"}, "alias-756": {"$ref": "alias-755"}, "alias-757": {"$ref": "alias-756"}, "alias-758": {"$ref": "alias-757"}, "alias-759": {"$ref": "alias-758"}, "alias-760": {"$ref": "alias-759"}, "alias-761": {"$ref": "alias-760"}, "alias-762": {"$ref": "alias-761"}, "alias-763": {"$ref": "alias-762"}, "alias-764": {"$ref": "alias-763"}, "alias-765": {"$ref": "alias-764"}, "alias-766": {"$ref": "alias-765"}, "alias-767": {"$ref": "alias-766"}, "alias-768": {"$ref": "alias-767"}, "alias-769": {"$ref": "alias-768"}, "alias-770": {"$ref": "alias-769"}, "alias-771": {"$ref": "alias-770"}, "alias-772": {"$ref": "alias-771"}, "alias-773": {"$ref": "alias-772"}, "alias-774": {"$ref": "alias-773"}, "alias-775": {"$ref": "alias-774"}, "alias-776": {"$ref": "alias-775"}, "alias-777": {"$ref": "alias-776"}, "alias-778": {"$ref": "alias-777"}, "alias-779": {"$ref": "alias-778"}, "alias-780": {"$ref": "alias-779"}, "alias-781": {"$ref": "alias-780"}, "alias-782": {"$ref": "alias-781"}, "alias-783": {"$ref": "alias-782"}, "alias-784": {"$ref": "alias-783"}, "alias-785": {"$ref": "alias-784"}, "alias-786": {"$ref": "alias-785"}, "alias-787": {"$ref": "alias-786"}, "alias-788": {"$ref": "alias-787"}, "alias-789": {"$ref": "alias-788"}, "alias-790": {"$ref": "alias-789"}, "alias-791": {"$ref": "alias-790"}, "alias-792": {"$ref": "alias-791"}, "alias-793": {"$ref": "alias-792"}, "alias-794": {"$ref": "alias-793"}, "alias-795": {"$ref": "alias-794"}, "alias-796": {"$ref": "alias-795"}, "alias-797": {"$ref": "alias-796"}, "alias-798": {"$ref": "alias-797"}, "alias-799": {"$ref": "alias-798"}, "alias-800": {"$ref": "alias-799"}}}, "flag": "ref-cost-under", "type": "boolean", "default": false, "targetingKey": "u-37", "context": {"tier": "gold"}, "expect": {"value": true, "variant": "on", "reason": "TARGETING_MATCH"}},
+ {"name": "references beyond the budget once each resolved reference costs one", "kind": "evaluate", "document": {"flags": {"ref-cost-under": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "alias-700"}, "on", "off"]}}, "ref-cost-over": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "alias-800"}, "on", "off"]}}}, "$evaluators": {"fan-0": {"==": [{"var": "tier"}, "gold"]}, "fan-1": {"or": [{"$ref": "fan-0"}, {"$ref": "fan-0"}]}, "fan-2": {"or": [{"$ref": "fan-1"}, {"$ref": "fan-1"}]}, "fan-3": {"or": [{"$ref": "fan-2"}, {"$ref": "fan-2"}]}, "fan-4": {"or": [{"$ref": "fan-3"}, {"$ref": "fan-3"}]}, "fan-5": {"or": [{"$ref": "fan-4"}, {"$ref": "fan-4"}]}, "fan-6": {"or": [{"$ref": "fan-5"}, {"$ref": "fan-5"}]}, "fan-7": {"or": [{"$ref": "fan-6"}, {"$ref": "fan-6"}]}, "fan-8": {"or": [{"$ref": "fan-7"}, {"$ref": "fan-7"}]}, "fan-9": {"or": [{"$ref": "fan-8"}, {"$ref": "fan-8"}]}, "fan-10": {"or": [{"$ref": "fan-9"}, {"$ref": "fan-9"}]}, "alias-0": {"$ref": "fan-10"}, "alias-1": {"$ref": "alias-0"}, "alias-2": {"$ref": "alias-1"}, "alias-3": {"$ref": "alias-2"}, "alias-4": {"$ref": "alias-3"}, "alias-5": {"$ref": "alias-4"}, "alias-6": {"$ref": "alias-5"}, "alias-7": {"$ref": "alias-6"}, "alias-8": {"$ref": "alias-7"}, "alias-9": {"$ref": "alias-8"}, "alias-10": {"$ref": "alias-9"}, "alias-11": {"$ref": "alias-10"}, "alias-12": {"$ref": "alias-11"}, "alias-13": {"$ref": "alias-12"}, "alias-14": {"$ref": "alias-13"}, "alias-15": {"$ref": "alias-14"}, "alias-16": {"$ref": "alias-15"}, "alias-17": {"$ref": "alias-16"}, "alias-18": {"$ref": "alias-17"}, "alias-19": {"$ref": "alias-18"}, "alias-20": {"$ref": "alias-19"}, "alias-21": {"$ref": "alias-20"}, "alias-22": {"$ref": "alias-21"}, "alias-23": {"$ref": "alias-22"}, "alias-24": {"$ref": "alias-23"}, "alias-25": {"$ref": "alias-24"}, "alias-26": {"$ref": "alias-25"}, "alias-27": {"$ref": "alias-26"}, "alias-28": {"$ref": "alias-27"}, "alias-29": {"$ref": "alias-28"}, "alias-30": {"$ref": "alias-29"}, "alias-31": {"$ref": "alias-30"}, "alias-32": {"$ref": "alias-31"}, "alias-33": {"$ref": "alias-32"}, "alias-34": {"$ref": "alias-33"}, "alias-35": {"$ref": "alias-34"}, "alias-36": {"$ref": "alias-35"}, "alias-37": {"$ref": "alias-36"}, "alias-38": {"$ref": "alias-37"}, "alias-39": {"$ref": "alias-38"}, "alias-40": {"$ref": "alias-39"}, "alias-41": {"$ref": "alias-40"}, "alias-42": {"$ref": "alias-41"}, "alias-43": {"$ref": "alias-42"}, "alias-44": {"$ref": "alias-43"}, "alias-45": {"$ref": "alias-44"}, "alias-46": {"$ref": "alias-45"}, "alias-47": {"$ref": "alias-46"}, "alias-48": {"$ref": "alias-47"}, "alias-49": {"$ref": "alias-48"}, "alias-50": {"$ref": "alias-49"}, "alias-51": {"$ref": "alias-50"}, "alias-52": {"$ref": "alias-51"}, "alias-53": {"$ref": "alias-52"}, "alias-54": {"$ref": "alias-53"}, "alias-55": {"$ref": "alias-54"}, "alias-56": {"$ref": "alias-55"}, "alias-57": {"$ref": "alias-56"}, "alias-58": {"$ref": "alias-57"}, "alias-59": {"$ref": "alias-58"}, "alias-60": {"$ref": "alias-59"}, "alias-61": {"$ref": "alias-60"}, "alias-62": {"$ref": "alias-61"}, "alias-63": {"$ref": "alias-62"}, "alias-64": {"$ref": "alias-63"}, "alias-65": {"$ref": "alias-64"}, "alias-66": {"$ref": "alias-65"}, "alias-67": {"$ref": "alias-66"}, "alias-68": {"$ref": "alias-67"}, "alias-69": {"$ref": "alias-68"}, "alias-70": {"$ref": "alias-69"}, "alias-71": {"$ref": "alias-70"}, "alias-72": {"$ref": "alias-71"}, "alias-73": {"$ref": "alias-72"}, "alias-74": {"$ref": "alias-73"}, "alias-75": {"$ref": "alias-74"}, "alias-76": {"$ref": "alias-75"}, "alias-77": {"$ref": "alias-76"}, "alias-78": {"$ref": "alias-77"}, "alias-79": {"$ref": "alias-78"}, "alias-80": {"$ref": "alias-79"}, "alias-81": {"$ref": "alias-80"}, "alias-82": {"$ref": "alias-81"}, "alias-83": {"$ref": "alias-82"}, "alias-84": {"$ref": "alias-83"}, "alias-85": {"$ref": "alias-84"}, "alias-86": {"$ref": "alias-85"}, "alias-87": {"$ref": "alias-86"}, "alias-88": {"$ref": "alias-87"}, "alias-89": {"$ref": "alias-88"}, "alias-90": {"$ref": "alias-89"}, "alias-91": {"$ref": "alias-90"}, "alias-92": {"$ref": "alias-91"}, "alias-93": {"$ref": "alias-92"}, "alias-94": {"$ref": "alias-93"}, "alias-95": {"$ref": "alias-94"}, "alias-96": {"$ref": "alias-95"}, "alias-97": {"$ref": "alias-96"}, "alias-98": {"$ref": "alias-97"}, "alias-99": {"$ref": "alias-98"}, "alias-100": {"$ref": "alias-99"}, "alias-101": {"$ref": "alias-100"}, "alias-102": {"$ref": "alias-101"}, "alias-103": {"$ref": "alias-102"}, "alias-104": {"$ref": "alias-103"}, "alias-105": {"$ref": "alias-104"}, "alias-106": {"$ref": "alias-105"}, "alias-107": {"$ref": "alias-106"}, "alias-108": {"$ref": "alias-107"}, "alias-109": {"$ref": "alias-108"}, "alias-110": {"$ref": "alias-109"}, "alias-111": {"$ref": "alias-110"}, "alias-112": {"$ref": "alias-111"}, "alias-113": {"$ref": "alias-112"}, "alias-114": {"$ref": "alias-113"}, "alias-115": {"$ref": "alias-114"}, "alias-116": {"$ref": "alias-115"}, "alias-117": {"$ref": "alias-116"}, "alias-118": {"$ref": "alias-117"}, "alias-119": {"$ref": "alias-118"}, "alias-120": {"$ref": "alias-119"}, "alias-121": {"$ref": "alias-120"}, "alias-122": {"$ref": "alias-121"}, "alias-123": {"$ref": "alias-122"}, "alias-124": {"$ref": "alias-123"}, "alias-125": {"$ref": "alias-124"}, "alias-126": {"$ref": "alias-125"}, "alias-127": {"$ref": "alias-126"}, "alias-128": {"$ref": "alias-127"}, "alias-129": {"$ref": "alias-128"}, "alias-130": {"$ref": "alias-129"}, "alias-131": {"$ref": "alias-130"}, "alias-132": {"$ref": "alias-131"}, "alias-133": {"$ref": "alias-132"}, "alias-134": {"$ref": "alias-133"}, "alias-135": {"$ref": "alias-134"}, "alias-136": {"$ref": "alias-135"}, "alias-137": {"$ref": "alias-136"}, "alias-138": {"$ref": "alias-137"}, "alias-139": {"$ref": "alias-138"}, "alias-140": {"$ref": "alias-139"}, "alias-141": {"$ref": "alias-140"}, "alias-142": {"$ref": "alias-141"}, "alias-143": {"$ref": "alias-142"}, "alias-144": {"$ref": "alias-143"}, "alias-145": {"$ref": "alias-144"}, "alias-146": {"$ref": "alias-145"}, "alias-147": {"$ref": "alias-146"}, "alias-148": {"$ref": "alias-147"}, "alias-149": {"$ref": "alias-148"}, "alias-150": {"$ref": "alias-149"}, "alias-151": {"$ref": "alias-150"}, "alias-152": {"$ref": "alias-151"}, "alias-153": {"$ref": "alias-152"}, "alias-154": {"$ref": "alias-153"}, "alias-155": {"$ref": "alias-154"}, "alias-156": {"$ref": "alias-155"}, "alias-157": {"$ref": "alias-156"}, "alias-158": {"$ref": "alias-157"}, "alias-159": {"$ref": "alias-158"}, "alias-160": {"$ref": "alias-159"}, "alias-161": {"$ref": "alias-160"}, "alias-162": {"$ref": "alias-161"}, "alias-163": {"$ref": "alias-162"}, "alias-164": {"$ref": "alias-163"}, "alias-165": {"$ref": "alias-164"}, "alias-166": {"$ref": "alias-165"}, "alias-167": {"$ref": "alias-166"}, "alias-168": {"$ref": "alias-167"}, "alias-169": {"$ref": "alias-168"}, "alias-170": {"$ref": "alias-169"}, "alias-171": {"$ref": "alias-170"}, "alias-172": {"$ref": "alias-171"}, "alias-173": {"$ref": "alias-172"}, "alias-174": {"$ref": "alias-173"}, "alias-175": {"$ref": "alias-174"}, "alias-176": {"$ref": "alias-175"}, "alias-177": {"$ref": "alias-176"}, "alias-178": {"$ref": "alias-177"}, "alias-179": {"$ref": "alias-178"}, "alias-180": {"$ref": "alias-179"}, "alias-181": {"$ref": "alias-180"}, "alias-182": {"$ref": "alias-181"}, "alias-183": {"$ref": "alias-182"}, "alias-184": {"$ref": "alias-183"}, "alias-185": {"$ref": "alias-184"}, "alias-186": {"$ref": "alias-185"}, "alias-187": {"$ref": "alias-186"}, "alias-188": {"$ref": "alias-187"}, "alias-189": {"$ref": "alias-188"}, "alias-190": {"$ref": "alias-189"}, "alias-191": {"$ref": "alias-190"}, "alias-192": {"$ref": "alias-191"}, "alias-193": {"$ref": "alias-192"}, "alias-194": {"$ref": "alias-193"}, "alias-195": {"$ref": "alias-194"}, "alias-196": {"$ref": "alias-195"}, "alias-197": {"$ref": "alias-196"}, "alias-198": {"$ref": "alias-197"}, "alias-199": {"$ref": "alias-198"}, "alias-200": {"$ref": "alias-199"}, "alias-201": {"$ref": "alias-200"}, "alias-202": {"$ref": "alias-201"}, "alias-203": {"$ref": "alias-202"}, "alias-204": {"$ref": "alias-203"}, "alias-205": {"$ref": "alias-204"}, "alias-206": {"$ref": "alias-205"}, "alias-207": {"$ref": "alias-206"}, "alias-208": {"$ref": "alias-207"}, "alias-209": {"$ref": "alias-208"}, "alias-210": {"$ref": "alias-209"}, "alias-211": {"$ref": "alias-210"}, "alias-212": {"$ref": "alias-211"}, "alias-213": {"$ref": "alias-212"}, "alias-214": {"$ref": "alias-213"}, "alias-215": {"$ref": "alias-214"}, "alias-216": {"$ref": "alias-215"}, "alias-217": {"$ref": "alias-216"}, "alias-218": {"$ref": "alias-217"}, "alias-219": {"$ref": "alias-218"}, "alias-220": {"$ref": "alias-219"}, "alias-221": {"$ref": "alias-220"}, "alias-222": {"$ref": "alias-221"}, "alias-223": {"$ref": "alias-222"}, "alias-224": {"$ref": "alias-223"}, "alias-225": {"$ref": "alias-224"}, "alias-226": {"$ref": "alias-225"}, "alias-227": {"$ref": "alias-226"}, "alias-228": {"$ref": "alias-227"}, "alias-229": {"$ref": "alias-228"}, "alias-230": {"$ref": "alias-229"}, "alias-231": {"$ref": "alias-230"}, "alias-232": {"$ref": "alias-231"}, "alias-233": {"$ref": "alias-232"}, "alias-234": {"$ref": "alias-233"}, "alias-235": {"$ref": "alias-234"}, "alias-236": {"$ref": "alias-235"}, "alias-237": {"$ref": "alias-236"}, "alias-238": {"$ref": "alias-237"}, "alias-239": {"$ref": "alias-238"}, "alias-240": {"$ref": "alias-239"}, "alias-241": {"$ref": "alias-240"}, "alias-242": {"$ref": "alias-241"}, "alias-243": {"$ref": "alias-242"}, "alias-244": {"$ref": "alias-243"}, "alias-245": {"$ref": "alias-244"}, "alias-246": {"$ref": "alias-245"}, "alias-247": {"$ref": "alias-246"}, "alias-248": {"$ref": "alias-247"}, "alias-249": {"$ref": "alias-248"}, "alias-250": {"$ref": "alias-249"}, "alias-251": {"$ref": "alias-250"}, "alias-252": {"$ref": "alias-251"}, "alias-253": {"$ref": "alias-252"}, "alias-254": {"$ref": "alias-253"}, "alias-255": {"$ref": "alias-254"}, "alias-256": {"$ref": "alias-255"}, "alias-257": {"$ref": "alias-256"}, "alias-258": {"$ref": "alias-257"}, "alias-259": {"$ref": "alias-258"}, "alias-260": {"$ref": "alias-259"}, "alias-261": {"$ref": "alias-260"}, "alias-262": {"$ref": "alias-261"}, "alias-263": {"$ref": "alias-262"}, "alias-264": {"$ref": "alias-263"}, "alias-265": {"$ref": "alias-264"}, "alias-266": {"$ref": "alias-265"}, "alias-267": {"$ref": "alias-266"}, "alias-268": {"$ref": "alias-267"}, "alias-269": {"$ref": "alias-268"}, "alias-270": {"$ref": "alias-269"}, "alias-271": {"$ref": "alias-270"}, "alias-272": {"$ref": "alias-271"}, "alias-273": {"$ref": "alias-272"}, "alias-274": {"$ref": "alias-273"}, "alias-275": {"$ref": "alias-274"}, "alias-276": {"$ref": "alias-275"}, "alias-277": {"$ref": "alias-276"}, "alias-278": {"$ref": "alias-277"}, "alias-279": {"$ref": "alias-278"}, "alias-280": {"$ref": "alias-279"}, "alias-281": {"$ref": "alias-280"}, "alias-282": {"$ref": "alias-281"}, "alias-283": {"$ref": "alias-282"}, "alias-284": {"$ref": "alias-283"}, "alias-285": {"$ref": "alias-284"}, "alias-286": {"$ref": "alias-285"}, "alias-287": {"$ref": "alias-286"}, "alias-288": {"$ref": "alias-287"}, "alias-289": {"$ref": "alias-288"}, "alias-290": {"$ref": "alias-289"}, "alias-291": {"$ref": "alias-290"}, "alias-292": {"$ref": "alias-291"}, "alias-293": {"$ref": "alias-292"}, "alias-294": {"$ref": "alias-293"}, "alias-295": {"$ref": "alias-294"}, "alias-296": {"$ref": "alias-295"}, "alias-297": {"$ref": "alias-296"}, "alias-298": {"$ref": "alias-297"}, "alias-299": {"$ref": "alias-298"}, "alias-300": {"$ref": "alias-299"}, "alias-301": {"$ref": "alias-300"}, "alias-302": {"$ref": "alias-301"}, "alias-303": {"$ref": "alias-302"}, "alias-304": {"$ref": "alias-303"}, "alias-305": {"$ref": "alias-304"}, "alias-306": {"$ref": "alias-305"}, "alias-307": {"$ref": "alias-306"}, "alias-308": {"$ref": "alias-307"}, "alias-309": {"$ref": "alias-308"}, "alias-310": {"$ref": "alias-309"}, "alias-311": {"$ref": "alias-310"}, "alias-312": {"$ref": "alias-311"}, "alias-313": {"$ref": "alias-312"}, "alias-314": {"$ref": "alias-313"}, "alias-315": {"$ref": "alias-314"}, "alias-316": {"$ref": "alias-315"}, "alias-317": {"$ref": "alias-316"}, "alias-318": {"$ref": "alias-317"}, "alias-319": {"$ref": "alias-318"}, "alias-320": {"$ref": "alias-319"}, "alias-321": {"$ref": "alias-320"}, "alias-322": {"$ref": "alias-321"}, "alias-323": {"$ref": "alias-322"}, "alias-324": {"$ref": "alias-323"}, "alias-325": {"$ref": "alias-324"}, "alias-326": {"$ref": "alias-325"}, "alias-327": {"$ref": "alias-326"}, "alias-328": {"$ref": "alias-327"}, "alias-329": {"$ref": "alias-328"}, "alias-330": {"$ref": "alias-329"}, "alias-331": {"$ref": "alias-330"}, "alias-332": {"$ref": "alias-331"}, "alias-333": {"$ref": "alias-332"}, "alias-334": {"$ref": "alias-333"}, "alias-335": {"$ref": "alias-334"}, "alias-336": {"$ref": "alias-335"}, "alias-337": {"$ref": "alias-336"}, "alias-338": {"$ref": "alias-337"}, "alias-339": {"$ref": "alias-338"}, "alias-340": {"$ref": "alias-339"}, "alias-341": {"$ref": "alias-340"}, "alias-342": {"$ref": "alias-341"}, "alias-343": {"$ref": "alias-342"}, "alias-344": {"$ref": "alias-343"}, "alias-345": {"$ref": "alias-344"}, "alias-346": {"$ref": "alias-345"}, "alias-347": {"$ref": "alias-346"}, "alias-348": {"$ref": "alias-347"}, "alias-349": {"$ref": "alias-348"}, "alias-350": {"$ref": "alias-349"}, "alias-351": {"$ref": "alias-350"}, "alias-352": {"$ref": "alias-351"}, "alias-353": {"$ref": "alias-352"}, "alias-354": {"$ref": "alias-353"}, "alias-355": {"$ref": "alias-354"}, "alias-356": {"$ref": "alias-355"}, "alias-357": {"$ref": "alias-356"}, "alias-358": {"$ref": "alias-357"}, "alias-359": {"$ref": "alias-358"}, "alias-360": {"$ref": "alias-359"}, "alias-361": {"$ref": "alias-360"}, "alias-362": {"$ref": "alias-361"}, "alias-363": {"$ref": "alias-362"}, "alias-364": {"$ref": "alias-363"}, "alias-365": {"$ref": "alias-364"}, "alias-366": {"$ref": "alias-365"}, "alias-367": {"$ref": "alias-366"}, "alias-368": {"$ref": "alias-367"}, "alias-369": {"$ref": "alias-368"}, "alias-370": {"$ref": "alias-369"}, "alias-371": {"$ref": "alias-370"}, "alias-372": {"$ref": "alias-371"}, "alias-373": {"$ref": "alias-372"}, "alias-374": {"$ref": "alias-373"}, "alias-375": {"$ref": "alias-374"}, "alias-376": {"$ref": "alias-375"}, "alias-377": {"$ref": "alias-376"}, "alias-378": {"$ref": "alias-377"}, "alias-379": {"$ref": "alias-378"}, "alias-380": {"$ref": "alias-379"}, "alias-381": {"$ref": "alias-380"}, "alias-382": {"$ref": "alias-381"}, "alias-383": {"$ref": "alias-382"}, "alias-384": {"$ref": "alias-383"}, "alias-385": {"$ref": "alias-384"}, "alias-386": {"$ref": "alias-385"}, "alias-387": {"$ref": "alias-386"}, "alias-388": {"$ref": "alias-387"}, "alias-389": {"$ref": "alias-388"}, "alias-390": {"$ref": "alias-389"}, "alias-391": {"$ref": "alias-390"}, "alias-392": {"$ref": "alias-391"}, "alias-393": {"$ref": "alias-392"}, "alias-394": {"$ref": "alias-393"}, "alias-395": {"$ref": "alias-394"}, "alias-396": {"$ref": "alias-395"}, "alias-397": {"$ref": "alias-396"}, "alias-398": {"$ref": "alias-397"}, "alias-399": {"$ref": "alias-398"}, "alias-400": {"$ref": "alias-399"}, "alias-401": {"$ref": "alias-400"}, "alias-402": {"$ref": "alias-401"}, "alias-403": {"$ref": "alias-402"}, "alias-404": {"$ref": "alias-403"}, "alias-405": {"$ref": "alias-404"}, "alias-406": {"$ref": "alias-405"}, "alias-407": {"$ref": "alias-406"}, "alias-408": {"$ref": "alias-407"}, "alias-409": {"$ref": "alias-408"}, "alias-410": {"$ref": "alias-409"}, "alias-411": {"$ref": "alias-410"}, "alias-412": {"$ref": "alias-411"}, "alias-413": {"$ref": "alias-412"}, "alias-414": {"$ref": "alias-413"}, "alias-415": {"$ref": "alias-414"}, "alias-416": {"$ref": "alias-415"}, "alias-417": {"$ref": "alias-416"}, "alias-418": {"$ref": "alias-417"}, "alias-419": {"$ref": "alias-418"}, "alias-420": {"$ref": "alias-419"}, "alias-421": {"$ref": "alias-420"}, "alias-422": {"$ref": "alias-421"}, "alias-423": {"$ref": "alias-422"}, "alias-424": {"$ref": "alias-423"}, "alias-425": {"$ref": "alias-424"}, "alias-426": {"$ref": "alias-425"}, "alias-427": {"$ref": "alias-426"}, "alias-428": {"$ref": "alias-427"}, "alias-429": {"$ref": "alias-428"}, "alias-430": {"$ref": "alias-429"}, "alias-431": {"$ref": "alias-430"}, "alias-432": {"$ref": "alias-431"}, "alias-433": {"$ref": "alias-432"}, "alias-434": {"$ref": "alias-433"}, "alias-435": {"$ref": "alias-434"}, "alias-436": {"$ref": "alias-435"}, "alias-437": {"$ref": "alias-436"}, "alias-438": {"$ref": "alias-437"}, "alias-439": {"$ref": "alias-438"}, "alias-440": {"$ref": "alias-439"}, "alias-441": {"$ref": "alias-440"}, "alias-442": {"$ref": "alias-441"}, "alias-443": {"$ref": "alias-442"}, "alias-444": {"$ref": "alias-443"}, "alias-445": {"$ref": "alias-444"}, "alias-446": {"$ref": "alias-445"}, "alias-447": {"$ref": "alias-446"}, "alias-448": {"$ref": "alias-447"}, "alias-449": {"$ref": "alias-448"}, "alias-450": {"$ref": "alias-449"}, "alias-451": {"$ref": "alias-450"}, "alias-452": {"$ref": "alias-451"}, "alias-453": {"$ref": "alias-452"}, "alias-454": {"$ref": "alias-453"}, "alias-455": {"$ref": "alias-454"}, "alias-456": {"$ref": "alias-455"}, "alias-457": {"$ref": "alias-456"}, "alias-458": {"$ref": "alias-457"}, "alias-459": {"$ref": "alias-458"}, "alias-460": {"$ref": "alias-459"}, "alias-461": {"$ref": "alias-460"}, "alias-462": {"$ref": "alias-461"}, "alias-463": {"$ref": "alias-462"}, "alias-464": {"$ref": "alias-463"}, "alias-465": {"$ref": "alias-464"}, "alias-466": {"$ref": "alias-465"}, "alias-467": {"$ref": "alias-466"}, "alias-468": {"$ref": "alias-467"}, "alias-469": {"$ref": "alias-468"}, "alias-470": {"$ref": "alias-469"}, "alias-471": {"$ref": "alias-470"}, "alias-472": {"$ref": "alias-471"}, "alias-473": {"$ref": "alias-472"}, "alias-474": {"$ref": "alias-473"}, "alias-475": {"$ref": "alias-474"}, "alias-476": {"$ref": "alias-475"}, "alias-477": {"$ref": "alias-476"}, "alias-478": {"$ref": "alias-477"}, "alias-479": {"$ref": "alias-478"}, "alias-480": {"$ref": "alias-479"}, "alias-481": {"$ref": "alias-480"}, "alias-482": {"$ref": "alias-481"}, "alias-483": {"$ref": "alias-482"}, "alias-484": {"$ref": "alias-483"}, "alias-485": {"$ref": "alias-484"}, "alias-486": {"$ref": "alias-485"}, "alias-487": {"$ref": "alias-486"}, "alias-488": {"$ref": "alias-487"}, "alias-489": {"$ref": "alias-488"}, "alias-490": {"$ref": "alias-489"}, "alias-491": {"$ref": "alias-490"}, "alias-492": {"$ref": "alias-491"}, "alias-493": {"$ref": "alias-492"}, "alias-494": {"$ref": "alias-493"}, "alias-495": {"$ref": "alias-494"}, "alias-496": {"$ref": "alias-495"}, "alias-497": {"$ref": "alias-496"}, "alias-498": {"$ref": "alias-497"}, "alias-499": {"$ref": "alias-498"}, "alias-500": {"$ref": "alias-499"}, "alias-501": {"$ref": "alias-500"}, "alias-502": {"$ref": "alias-501"}, "alias-503": {"$ref": "alias-502"}, "alias-504": {"$ref": "alias-503"}, "alias-505": {"$ref": "alias-504"}, "alias-506": {"$ref": "alias-505"}, "alias-507": {"$ref": "alias-506"}, "alias-508": {"$ref": "alias-507"}, "alias-509": {"$ref": "alias-508"}, "alias-510": {"$ref": "alias-509"}, "alias-511": {"$ref": "alias-510"}, "alias-512": {"$ref": "alias-511"}, "alias-513": {"$ref": "alias-512"}, "alias-514": {"$ref": "alias-513"}, "alias-515": {"$ref": "alias-514"}, "alias-516": {"$ref": "alias-515"}, "alias-517": {"$ref": "alias-516"}, "alias-518": {"$ref": "alias-517"}, "alias-519": {"$ref": "alias-518"}, "alias-520": {"$ref": "alias-519"}, "alias-521": {"$ref": "alias-520"}, "alias-522": {"$ref": "alias-521"}, "alias-523": {"$ref": "alias-522"}, "alias-524": {"$ref": "alias-523"}, "alias-525": {"$ref": "alias-524"}, "alias-526": {"$ref": "alias-525"}, "alias-527": {"$ref": "alias-526"}, "alias-528": {"$ref": "alias-527"}, "alias-529": {"$ref": "alias-528"}, "alias-530": {"$ref": "alias-529"}, "alias-531": {"$ref": "alias-530"}, "alias-532": {"$ref": "alias-531"}, "alias-533": {"$ref": "alias-532"}, "alias-534": {"$ref": "alias-533"}, "alias-535": {"$ref": "alias-534"}, "alias-536": {"$ref": "alias-535"}, "alias-537": {"$ref": "alias-536"}, "alias-538": {"$ref": "alias-537"}, "alias-539": {"$ref": "alias-538"}, "alias-540": {"$ref": "alias-539"}, "alias-541": {"$ref": "alias-540"}, "alias-542": {"$ref": "alias-541"}, "alias-543": {"$ref": "alias-542"}, "alias-544": {"$ref": "alias-543"}, "alias-545": {"$ref": "alias-544"}, "alias-546": {"$ref": "alias-545"}, "alias-547": {"$ref": "alias-546"}, "alias-548": {"$ref": "alias-547"}, "alias-549": {"$ref": "alias-548"}, "alias-550": {"$ref": "alias-549"}, "alias-551": {"$ref": "alias-550"}, "alias-552": {"$ref": "alias-551"}, "alias-553": {"$ref": "alias-552"}, "alias-554": {"$ref": "alias-553"}, "alias-555": {"$ref": "alias-554"}, "alias-556": {"$ref": "alias-555"}, "alias-557": {"$ref": "alias-556"}, "alias-558": {"$ref": "alias-557"}, "alias-559": {"$ref": "alias-558"}, "alias-560": {"$ref": "alias-559"}, "alias-561": {"$ref": "alias-560"}, "alias-562": {"$ref": "alias-561"}, "alias-563": {"$ref": "alias-562"}, "alias-564": {"$ref": "alias-563"}, "alias-565": {"$ref": "alias-564"}, "alias-566": {"$ref": "alias-565"}, "alias-567": {"$ref": "alias-566"}, "alias-568": {"$ref": "alias-567"}, "alias-569": {"$ref": "alias-568"}, "alias-570": {"$ref": "alias-569"}, "alias-571": {"$ref": "alias-570"}, "alias-572": {"$ref": "alias-571"}, "alias-573": {"$ref": "alias-572"}, "alias-574": {"$ref": "alias-573"}, "alias-575": {"$ref": "alias-574"}, "alias-576": {"$ref": "alias-575"}, "alias-577": {"$ref": "alias-576"}, "alias-578": {"$ref": "alias-577"}, "alias-579": {"$ref": "alias-578"}, "alias-580": {"$ref": "alias-579"}, "alias-581": {"$ref": "alias-580"}, "alias-582": {"$ref": "alias-581"}, "alias-583": {"$ref": "alias-582"}, "alias-584": {"$ref": "alias-583"}, "alias-585": {"$ref": "alias-584"}, "alias-586": {"$ref": "alias-585"}, "alias-587": {"$ref": "alias-586"}, "alias-588": {"$ref": "alias-587"}, "alias-589": {"$ref": "alias-588"}, "alias-590": {"$ref": "alias-589"}, "alias-591": {"$ref": "alias-590"}, "alias-592": {"$ref": "alias-591"}, "alias-593": {"$ref": "alias-592"}, "alias-594": {"$ref": "alias-593"}, "alias-595": {"$ref": "alias-594"}, "alias-596": {"$ref": "alias-595"}, "alias-597": {"$ref": "alias-596"}, "alias-598": {"$ref": "alias-597"}, "alias-599": {"$ref": "alias-598"}, "alias-600": {"$ref": "alias-599"}, "alias-601": {"$ref": "alias-600"}, "alias-602": {"$ref": "alias-601"}, "alias-603": {"$ref": "alias-602"}, "alias-604": {"$ref": "alias-603"}, "alias-605": {"$ref": "alias-604"}, "alias-606": {"$ref": "alias-605"}, "alias-607": {"$ref": "alias-606"}, "alias-608": {"$ref": "alias-607"}, "alias-609": {"$ref": "alias-608"}, "alias-610": {"$ref": "alias-609"}, "alias-611": {"$ref": "alias-610"}, "alias-612": {"$ref": "alias-611"}, "alias-613": {"$ref": "alias-612"}, "alias-614": {"$ref": "alias-613"}, "alias-615": {"$ref": "alias-614"}, "alias-616": {"$ref": "alias-615"}, "alias-617": {"$ref": "alias-616"}, "alias-618": {"$ref": "alias-617"}, "alias-619": {"$ref": "alias-618"}, "alias-620": {"$ref": "alias-619"}, "alias-621": {"$ref": "alias-620"}, "alias-622": {"$ref": "alias-621"}, "alias-623": {"$ref": "alias-622"}, "alias-624": {"$ref": "alias-623"}, "alias-625": {"$ref": "alias-624"}, "alias-626": {"$ref": "alias-625"}, "alias-627": {"$ref": "alias-626"}, "alias-628": {"$ref": "alias-627"}, "alias-629": {"$ref": "alias-628"}, "alias-630": {"$ref": "alias-629"}, "alias-631": {"$ref": "alias-630"}, "alias-632": {"$ref": "alias-631"}, "alias-633": {"$ref": "alias-632"}, "alias-634": {"$ref": "alias-633"}, "alias-635": {"$ref": "alias-634"}, "alias-636": {"$ref": "alias-635"}, "alias-637": {"$ref": "alias-636"}, "alias-638": {"$ref": "alias-637"}, "alias-639": {"$ref": "alias-638"}, "alias-640": {"$ref": "alias-639"}, "alias-641": {"$ref": "alias-640"}, "alias-642": {"$ref": "alias-641"}, "alias-643": {"$ref": "alias-642"}, "alias-644": {"$ref": "alias-643"}, "alias-645": {"$ref": "alias-644"}, "alias-646": {"$ref": "alias-645"}, "alias-647": {"$ref": "alias-646"}, "alias-648": {"$ref": "alias-647"}, "alias-649": {"$ref": "alias-648"}, "alias-650": {"$ref": "alias-649"}, "alias-651": {"$ref": "alias-650"}, "alias-652": {"$ref": "alias-651"}, "alias-653": {"$ref": "alias-652"}, "alias-654": {"$ref": "alias-653"}, "alias-655": {"$ref": "alias-654"}, "alias-656": {"$ref": "alias-655"}, "alias-657": {"$ref": "alias-656"}, "alias-658": {"$ref": "alias-657"}, "alias-659": {"$ref": "alias-658"}, "alias-660": {"$ref": "alias-659"}, "alias-661": {"$ref": "alias-660"}, "alias-662": {"$ref": "alias-661"}, "alias-663": {"$ref": "alias-662"}, "alias-664": {"$ref": "alias-663"}, "alias-665": {"$ref": "alias-664"}, "alias-666": {"$ref": "alias-665"}, "alias-667": {"$ref": "alias-666"}, "alias-668": {"$ref": "alias-667"}, "alias-669": {"$ref": "alias-668"}, "alias-670": {"$ref": "alias-669"}, "alias-671": {"$ref": "alias-670"}, "alias-672": {"$ref": "alias-671"}, "alias-673": {"$ref": "alias-672"}, "alias-674": {"$ref": "alias-673"}, "alias-675": {"$ref": "alias-674"}, "alias-676": {"$ref": "alias-675"}, "alias-677": {"$ref": "alias-676"}, "alias-678": {"$ref": "alias-677"}, "alias-679": {"$ref": "alias-678"}, "alias-680": {"$ref": "alias-679"}, "alias-681": {"$ref": "alias-680"}, "alias-682": {"$ref": "alias-681"}, "alias-683": {"$ref": "alias-682"}, "alias-684": {"$ref": "alias-683"}, "alias-685": {"$ref": "alias-684"}, "alias-686": {"$ref": "alias-685"}, "alias-687": {"$ref": "alias-686"}, "alias-688": {"$ref": "alias-687"}, "alias-689": {"$ref": "alias-688"}, "alias-690": {"$ref": "alias-689"}, "alias-691": {"$ref": "alias-690"}, "alias-692": {"$ref": "alias-691"}, "alias-693": {"$ref": "alias-692"}, "alias-694": {"$ref": "alias-693"}, "alias-695": {"$ref": "alias-694"}, "alias-696": {"$ref": "alias-695"}, "alias-697": {"$ref": "alias-696"}, "alias-698": {"$ref": "alias-697"}, "alias-699": {"$ref": "alias-698"}, "alias-700": {"$ref": "alias-699"}, "alias-701": {"$ref": "alias-700"}, "alias-702": {"$ref": "alias-701"}, "alias-703": {"$ref": "alias-702"}, "alias-704": {"$ref": "alias-703"}, "alias-705": {"$ref": "alias-704"}, "alias-706": {"$ref": "alias-705"}, "alias-707": {"$ref": "alias-706"}, "alias-708": {"$ref": "alias-707"}, "alias-709": {"$ref": "alias-708"}, "alias-710": {"$ref": "alias-709"}, "alias-711": {"$ref": "alias-710"}, "alias-712": {"$ref": "alias-711"}, "alias-713": {"$ref": "alias-712"}, "alias-714": {"$ref": "alias-713"}, "alias-715": {"$ref": "alias-714"}, "alias-716": {"$ref": "alias-715"}, "alias-717": {"$ref": "alias-716"}, "alias-718": {"$ref": "alias-717"}, "alias-719": {"$ref": "alias-718"}, "alias-720": {"$ref": "alias-719"}, "alias-721": {"$ref": "alias-720"}, "alias-722": {"$ref": "alias-721"}, "alias-723": {"$ref": "alias-722"}, "alias-724": {"$ref": "alias-723"}, "alias-725": {"$ref": "alias-724"}, "alias-726": {"$ref": "alias-725"}, "alias-727": {"$ref": "alias-726"}, "alias-728": {"$ref": "alias-727"}, "alias-729": {"$ref": "alias-728"}, "alias-730": {"$ref": "alias-729"}, "alias-731": {"$ref": "alias-730"}, "alias-732": {"$ref": "alias-731"}, "alias-733": {"$ref": "alias-732"}, "alias-734": {"$ref": "alias-733"}, "alias-735": {"$ref": "alias-734"}, "alias-736": {"$ref": "alias-735"}, "alias-737": {"$ref": "alias-736"}, "alias-738": {"$ref": "alias-737"}, "alias-739": {"$ref": "alias-738"}, "alias-740": {"$ref": "alias-739"}, "alias-741": {"$ref": "alias-740"}, "alias-742": {"$ref": "alias-741"}, "alias-743": {"$ref": "alias-742"}, "alias-744": {"$ref": "alias-743"}, "alias-745": {"$ref": "alias-744"}, "alias-746": {"$ref": "alias-745"}, "alias-747": {"$ref": "alias-746"}, "alias-748": {"$ref": "alias-747"}, "alias-749": {"$ref": "alias-748"}, "alias-750": {"$ref": "alias-749"}, "alias-751": {"$ref": "alias-750"}, "alias-752": {"$ref": "alias-751"}, "alias-753": {"$ref": "alias-752"}, "alias-754": {"$ref": "alias-753"}, "alias-755": {"$ref": "alias-754"}, "alias-756": {"$ref": "alias-755"}, "alias-757": {"$ref": "alias-756"}, "alias-758": {"$ref": "alias-757"}, "alias-759": {"$ref": "alias-758"}, "alias-760": {"$ref": "alias-759"}, "alias-761": {"$ref": "alias-760"}, "alias-762": {"$ref": "alias-761"}, "alias-763": {"$ref": "alias-762"}, "alias-764": {"$ref": "alias-763"}, "alias-765": {"$ref": "alias-764"}, "alias-766": {"$ref": "alias-765"}, "alias-767": {"$ref": "alias-766"}, "alias-768": {"$ref": "alias-767"}, "alias-769": {"$ref": "alias-768"}, "alias-770": {"$ref": "alias-769"}, "alias-771": {"$ref": "alias-770"}, "alias-772": {"$ref": "alias-771"}, "alias-773": {"$ref": "alias-772"}, "alias-774": {"$ref": "alias-773"}, "alias-775": {"$ref": "alias-774"}, "alias-776": {"$ref": "alias-775"}, "alias-777": {"$ref": "alias-776"}, "alias-778": {"$ref": "alias-777"}, "alias-779": {"$ref": "alias-778"}, "alias-780": {"$ref": "alias-779"}, "alias-781": {"$ref": "alias-780"}, "alias-782": {"$ref": "alias-781"}, "alias-783": {"$ref": "alias-782"}, "alias-784": {"$ref": "alias-783"}, "alias-785": {"$ref": "alias-784"}, "alias-786": {"$ref": "alias-785"}, "alias-787": {"$ref": "alias-786"}, "alias-788": {"$ref": "alias-787"}, "alias-789": {"$ref": "alias-788"}, "alias-790": {"$ref": "alias-789"}, "alias-791": {"$ref": "alias-790"}, "alias-792": {"$ref": "alias-791"}, "alias-793": {"$ref": "alias-792"}, "alias-794": {"$ref": "alias-793"}, "alias-795": {"$ref": "alias-794"}, "alias-796": {"$ref": "alias-795"}, "alias-797": {"$ref": "alias-796"}, "alias-798": {"$ref": "alias-797"}, "alias-799": {"$ref": "alias-798"}, "alias-800": {"$ref": "alias-799"}}}, "flag": "ref-cost-over", "type": "boolean", "default": true, "targetingKey": "u-38", "context": {"tier": "gold"}, "expect": {"value": true, "variant": null, "reason": "ERROR", "errorCode": "PARSE_ERROR"}},
+ {"name": "a float request on a boolean flag is a type mismatch", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "kill-switch", "type": "float", "default": 0.5, "expect": {"value": 0.5, "variant": null, "reason": "ERROR", "errorCode": "TYPE_MISMATCH"}},
+ {"name": "an integer request on a boolean flag is a type mismatch", "kind": "evaluate", "document": {"flags": {"kill-switch": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "new-checkout": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": ["beta", {"var": "roles"}]}, "on", null]}, "metadata": {"owner": "payments", "kind": "release", "expires": "2026-12-31"}}, "beta-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "beta-users"}, "on", "off"]}}, "pro-reports": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"in": [{"var": "plan"}, ["pro", "enterprise"]]}, "on", "off"]}, "metadata": {"kind": "permission"}}, "tenant-pilot": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "on", null]}}, "checkout-flow": {"state": "ENABLED", "variants": {"control": "v1", "treatment": "v2", "holdout": "v1"}, "defaultVariant": "control", "targeting": {"fractional": [["control", 45], ["treatment", 45], ["holdout", 10]]}, "metadata": {"kind": "experiment"}}, "rollout-10": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"fractional": [["on", 10], ["off", 90]]}}, "min-app-version": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"sem_ver": [{"var": "appVersion"}, ">=", "2.4.0"]}, "on", "off"]}}, "email-domain": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"ends_with": [{"var": "email"}, "@firefly.dev"]}, "on", "off"]}}, "paused": {"state": "DISABLED", "variants": {"on": true, "off": false}, "defaultVariant": "on"}, "page-size": {"state": "ENABLED", "variants": {"small": 10, "large": 50}, "defaultVariant": "small", "targeting": {"if": [{"in": ["admin", {"var": "roles"}]}, "large", null]}}, "sample-rate": {"state": "ENABLED", "variants": {"low": 0.05, "high": 0.5}, "defaultVariant": "low"}, "banner": {"state": "ENABLED", "variants": {"plain": {"title": "Hello", "dismissible": true}, "promo": {"title": "50% off", "dismissible": false, "ttl": 3}}, "defaultVariant": "plain", "targeting": {"if": [{"==": [{"var": "profiles.0"}, "staging"]}, "promo", null]}}, "region-gate": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "eu"}, "on", "off"]}}, "no-default": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}, "targeting": {"if": [{"==": [{"var": "tenant"}, "acme"]}, "b", null]}}, "no-default-static": {"state": "ENABLED", "variants": {"a": "A", "b": "B"}}, "bad-variant": {"state": "ENABLED", "variants": {"a": "A"}, "defaultVariant": "a", "targeting": {"if": [true, "zzz", null]}}, "windows-path": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "temp-dir"}, "on", "off"]}}, "empty-default": {"state": "ENABLED", "variants": {"": "empty", "x": "X"}, "defaultVariant": ""}, "nested-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "z-staff-in-eu"}, "on", "off"]}}, "nested-segment-reverse": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "a-staff-in-eu"}, "on", "off"]}}, "cyclic-segment": {"state": "ENABLED", "variants": {"on": true, "off": false}, "defaultVariant": "off", "targeting": {"if": [{"$ref": "cycle-a"}, "on", "off"]}}}, "$evaluators": {"beta-users": {"in": ["beta", {"var": "roles"}]}, "eu": {"in": [{"var": "region"}, ["es", "fr", "de", "it", "pt"]]}, "temp-dir": {"==": [{"var": "path"}, "C:\\temp\\new"]}, "z-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "eu"}]}, "a-staff-in-eu": {"and": [{"in": ["staff", {"var": "roles"}]}, {"$ref": "zz-eu"}]}, "zz-eu": {"$ref": "eu"}, "cycle-a": {"or": [{"$ref": "cycle-b"}, false]}, "cycle-b": {"or": [{"$ref": "cycle-a"}, false]}}}, "flag": "kill-switch", "type": "integer", "default": 7, "expect": {"value": 7, "variant": null, "reason": "ERROR", "errorCode": "TYPE_MISMATCH"}},
+ {"name": "cross-language bucketing: rollout", "kind": "bucket", "flagKey": "rollout", "weights": [["on", 10], ["off", 90]], "keys": ["user-0001", "user-0002", "user-0003", "user-0004", "user-0005", "user-0006", "user-0007", "user-0008", "user-0009", "user-0010", "user-0011", "user-0012", "user-0013", "user-0014", "user-0015", "user-0016", "user-0017", "user-0018", "user-0019", "user-0020", "user-0021", "user-0022", "user-0023", "user-0024", "user-0025", "user-0026", "user-0027", "user-0028", "user-0029", "user-0030", "user-0031", "user-0032", "user-0033", "user-0034", "user-0035", "user-0036", "user-0037", "user-0038", "user-0039", "user-0040", "user-0041", "user-0042", "user-0043", "user-0044", "user-0045", "user-0046", "user-0047", "user-0048", "user-0049", "user-0050", "user-0051", "user-0052", "user-0053", "user-0054", "user-0055", "user-0056", "user-0057", "user-0058", "user-0059", "user-0060", "user-0061", "user-0062", "user-0063", "user-0064", "user-0065", "user-0066", "user-0067", "user-0068", "user-0069", "user-0070", "user-0071", "user-0072", "user-0073", "user-0074", "user-0075", "user-0076", "user-0077", "user-0078", "user-0079", "user-0080", "user-0081", "user-0082", "user-0083", "user-0084", "user-0085", "user-0086", "user-0087", "user-0088", "user-0089", "user-0090", "user-0091", "user-0092", "user-0093", "user-0094", "user-0095", "user-0096", "user-0097", "user-0098", "user-0099", "user-0100", "user-0101", "user-0102", "user-0103", "user-0104", "user-0105", "user-0106", "user-0107", "user-0108", "user-0109", "user-0110", "user-0111", "user-0112", "user-0113", "user-0114", "user-0115", "user-0116", "user-0117", "user-0118", "user-0119", "user-0120", "alice@example.com", "bob@example.com", "carol@example.com", "dave@example.com", "erin@example.com", "frank@example.com", "grace@example.com", "heidi@example.com", "ivan@example.com", "judy@example.com", "mallory@example.com", "niaj@example.com", "olivia@example.com", "peggy@example.com", "rupert@example.com", "sybil@example.com", "trent@example.com", "victor@example.com", "walter@example.com", "zoe@example.com", "9e3779b1-9e37-4061-a185-0100000001b3", "3c6ef362-3c6e-40c2-a30a-020000000366", "daa66d13-daa5-4123-a48f-030000000519", "78dde6c4-78dc-4184-a614-0400000006cc", "17156075-1713-41e5-a799-05000000087f", "b54cda26-b54a-4246-a91e-060000000a32", "538453d7-5381-42a7-aaa3-070000000be5", "f1bbcd88-f1b8-4308-ac28-080000000d98", "8ff34739-8fef-4369-adad-090000000f4b", "2e2ac0ea-2e26-43ca-af32-0a00000010fe", "cc623a9b-cc5d-442b-a0b7-0b00000012b1", "6a99b44c-6a94-448c-a23c-0c0000001464", "08d12dfd-08cb-44ed-a3c1-0d0000001617", "a708a7ae-a702-454e-a546-0e00000017ca", "4540215f-4539-45af-a6cb-0f000000197d", "e3779b10-e370-4610-a850-100000001b30", "81af14c1-81a7-4671-a9d5-110000001ce3", "1fe68e72-1fde-46d2-ab5a-120000001e96", "be1e0823-be15-4733-acdf-130000002049", "5c5581d4-5c4c-4794-ae64-1400000021fc", "fa8cfb85-fa83-47f5-afe9-1500000023af", "98c47536-98ba-4856-a16e-160000002562", "36fbeee7-36f1-48b7-a2f3-170000002715", "d5336898-d528-4918-a478-1800000028c8", "736ae249-735f-4979-a5fd-190000002a7b", "11a25bfa-1196-49da-a782-1a0000002c2e", "afd9d5ab-afcd-4a3b-a907-1b0000002de1", "4e114f5c-4e04-4a9c-aa8c-1c0000002f94", "ec48c90d-ec3b-4afd-ac11-1d0000003147", "8a8042be-8a72-4b5e-ad96-1e00000032fa", "28b7bc6f-28a9-4bbf-af1b-1f00000034ad", "c6ef3620-c6e0-4c20-a0a0-200000003660", "6526afd1-6517-4c81-a225-210000003813", "035e2982-034e-4ce2-a3aa-2200000039c6", "a195a333-a185-4d43-a52f-230000003b79", "3fcd1ce4-3fbc-4da4-a6b4-240000003d2c", "de049695-ddf3-4e05-a839-250000003edf", "7c3c1046-7c2a-4e66-a9be-260000004092", "1a7389f7-1a61-4ec7-ab43-270000004245", "b8ab03a8-b898-4f28-acc8-2800000043f8", "56e27d59-56cf-4f89-ae4d-2900000045ab", "f519f70a-f506-4fea-afd2-2a000000475e", "935170bb-933d-404b-a157-2b0000004911", "3188ea6c-3174-40ac-a2dc-2c0000004ac4", "cfc0641d-cfab-410d-a461-2d0000004c77", "6df7ddce-6de2-416e-a5e6-2e0000004e2a", "0c2f577f-0c19-41cf-a76b-2f0000004fdd", "aa66d130-aa50-4230-a8f0-300000005190", "489e4ae1-4887-4291-aa75-310000005343", "e6d5c492-e6be-42f2-abfa-3200000054f6", "850d3e43-84f5-4353-ad7f-3300000056a9", "2344b7f4-232c-43b4-af04-34000000585c", "c17c31a5-c163-4415-a089-350000005a0f", "5fb3ab56-5f9a-4476-a20e-360000005bc2", "fdeb2507-fdd1-44d7-a393-370000005d75", "9c229eb8-9c08-4538-a518-380000005f28", "3a5a1869-3a3f-4599-a69d-3900000060db", "d891921a-d876-45fa-a822-3a000000628e", "76c90bcb-76ad-465b-a9a7-3b0000006441", "1500857c-14e4-46bc-ab2c-3c00000065f4", "José", "Zoë", "Łukasz", "Ægir", "Søren", "François", "Müller", "Ñandú", "İstanbul", "東京", "서울", "Москва", "Αθήνα", "☃", "🚀rocket", "a", "ab", "", " ", "tenant/acme/user/1", "0", "1", "2", "3", "10", "99", "100", "1000", "65535", "2147483647", "4294967295", "-1", "tenant-acme:user-1", "tenant-acme:user-2", "tenant-acme:user-3", "tenant-acme:user-4", "tenant-acme:user-5", "tenant-acme:user-6", "tenant-acme:user-7", "tenant-acme:user-8", "tenant-acme:user-9", "tenant-acme:user-10", "tenant-acme:user-11", "tenant-acme:user-12", "tenant-acme:user-13", "tenant-acme:user-14", "tenant-acme:user-15", "tenant-acme:user-16", "tenant-acme:user-17", "tenant-acme:user-18", "tenant-acme:user-19", "tenant-acme:user-20", "tenant-acme:user-21", "tenant-acme:user-22", "tenant-acme:user-23", "tenant-globex:user-1", "tenant-globex:user-2", "tenant-globex:user-3", "tenant-globex:user-4", "tenant-globex:user-5", "tenant-globex:user-6", "tenant-globex:user-7", "tenant-globex:user-8", "tenant-globex:user-9", "tenant-globex:user-10", "tenant-globex:user-11", "tenant-globex:user-12", "tenant-globex:user-13", "tenant-globex:user-14", "tenant-globex:user-15", "tenant-globex:user-16", "tenant-globex:user-17", "tenant-globex:user-18", "tenant-globex:user-19", "tenant-globex:user-20", "tenant-globex:user-21", "tenant-globex:user-22", "tenant-globex:user-23", "tenant-initech:user-1", "tenant-initech:user-2", "tenant-initech:user-3", "tenant-initech:user-4", "tenant-initech:user-5", "tenant-initech:user-6", "tenant-initech:user-7", "tenant-initech:user-8", "tenant-initech:user-9", "tenant-initech:user-10", "tenant-initech:user-11", "tenant-initech:user-12", "tenant-initech:user-13", "tenant-initech:user-14", "tenant-initech:user-15", "tenant-initech:user-16", "tenant-initech:user-17", "tenant-initech:user-18", "tenant-initech:user-19", "tenant-initech:user-20", "tenant-initech:user-21", "tenant-initech:user-22", "tenant-initech:user-23"], "expect": ["off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "on", "off", "off", "off", "off", "on", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "on", "off", "off", "off", "off", "off", "off", "off", "off", "off", "on", "off", "off", "off", "on", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "on", "on", "off", "off", "off", "on", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "on", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "on", "off", "off", "off", "off", "off", "off", "off", "on", "on", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "on", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "on", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "on", "off", "off", "off", "off", "off", "off", "on", "off", "off", "on", "off", "off", "off", "off", "off", "off", "off", "off", "on", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "on", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "on", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "on", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "off", "on", "off", "off", "off", "off", "off", "on", "off", "off", "off", "off", "off"]},
+ {"name": "cross-language bucketing: ab-test", "kind": "bucket", "flagKey": "ab-test", "weights": [["a", 50], ["b", 50]], "keys": ["user-0001", "user-0002", "user-0003", "user-0004", "user-0005", "user-0006", "user-0007", "user-0008", "user-0009", "user-0010", "user-0011", "user-0012", "user-0013", "user-0014", "user-0015", "user-0016", "user-0017", "user-0018", "user-0019", "user-0020", "user-0021", "user-0022", "user-0023", "user-0024", "user-0025", "user-0026", "user-0027", "user-0028", "user-0029", "user-0030", "user-0031", "user-0032", "user-0033", "user-0034", "user-0035", "user-0036", "user-0037", "user-0038", "user-0039", "user-0040", "user-0041", "user-0042", "user-0043", "user-0044", "user-0045", "user-0046", "user-0047", "user-0048", "user-0049", "user-0050", "user-0051", "user-0052", "user-0053", "user-0054", "user-0055", "user-0056", "user-0057", "user-0058", "user-0059", "user-0060", "user-0061", "user-0062", "user-0063", "user-0064", "user-0065", "user-0066", "user-0067", "user-0068", "user-0069", "user-0070", "user-0071", "user-0072", "user-0073", "user-0074", "user-0075", "user-0076", "user-0077", "user-0078", "user-0079", "user-0080", "user-0081", "user-0082", "user-0083", "user-0084", "user-0085", "user-0086", "user-0087", "user-0088", "user-0089", "user-0090", "user-0091", "user-0092", "user-0093", "user-0094", "user-0095", "user-0096", "user-0097", "user-0098", "user-0099", "user-0100", "user-0101", "user-0102", "user-0103", "user-0104", "user-0105", "user-0106", "user-0107", "user-0108", "user-0109", "user-0110", "user-0111", "user-0112", "user-0113", "user-0114", "user-0115", "user-0116", "user-0117", "user-0118", "user-0119", "user-0120", "alice@example.com", "bob@example.com", "carol@example.com", "dave@example.com", "erin@example.com", "frank@example.com", "grace@example.com", "heidi@example.com", "ivan@example.com", "judy@example.com", "mallory@example.com", "niaj@example.com", "olivia@example.com", "peggy@example.com", "rupert@example.com", "sybil@example.com", "trent@example.com", "victor@example.com", "walter@example.com", "zoe@example.com", "9e3779b1-9e37-4061-a185-0100000001b3", "3c6ef362-3c6e-40c2-a30a-020000000366", "daa66d13-daa5-4123-a48f-030000000519", "78dde6c4-78dc-4184-a614-0400000006cc", "17156075-1713-41e5-a799-05000000087f", "b54cda26-b54a-4246-a91e-060000000a32", "538453d7-5381-42a7-aaa3-070000000be5", "f1bbcd88-f1b8-4308-ac28-080000000d98", "8ff34739-8fef-4369-adad-090000000f4b", "2e2ac0ea-2e26-43ca-af32-0a00000010fe", "cc623a9b-cc5d-442b-a0b7-0b00000012b1", "6a99b44c-6a94-448c-a23c-0c0000001464", "08d12dfd-08cb-44ed-a3c1-0d0000001617", "a708a7ae-a702-454e-a546-0e00000017ca", "4540215f-4539-45af-a6cb-0f000000197d", "e3779b10-e370-4610-a850-100000001b30", "81af14c1-81a7-4671-a9d5-110000001ce3", "1fe68e72-1fde-46d2-ab5a-120000001e96", "be1e0823-be15-4733-acdf-130000002049", "5c5581d4-5c4c-4794-ae64-1400000021fc", "fa8cfb85-fa83-47f5-afe9-1500000023af", "98c47536-98ba-4856-a16e-160000002562", "36fbeee7-36f1-48b7-a2f3-170000002715", "d5336898-d528-4918-a478-1800000028c8", "736ae249-735f-4979-a5fd-190000002a7b", "11a25bfa-1196-49da-a782-1a0000002c2e", "afd9d5ab-afcd-4a3b-a907-1b0000002de1", "4e114f5c-4e04-4a9c-aa8c-1c0000002f94", "ec48c90d-ec3b-4afd-ac11-1d0000003147", "8a8042be-8a72-4b5e-ad96-1e00000032fa", "28b7bc6f-28a9-4bbf-af1b-1f00000034ad", "c6ef3620-c6e0-4c20-a0a0-200000003660", "6526afd1-6517-4c81-a225-210000003813", "035e2982-034e-4ce2-a3aa-2200000039c6", "a195a333-a185-4d43-a52f-230000003b79", "3fcd1ce4-3fbc-4da4-a6b4-240000003d2c", "de049695-ddf3-4e05-a839-250000003edf", "7c3c1046-7c2a-4e66-a9be-260000004092", "1a7389f7-1a61-4ec7-ab43-270000004245", "b8ab03a8-b898-4f28-acc8-2800000043f8", "56e27d59-56cf-4f89-ae4d-2900000045ab", "f519f70a-f506-4fea-afd2-2a000000475e", "935170bb-933d-404b-a157-2b0000004911", "3188ea6c-3174-40ac-a2dc-2c0000004ac4", "cfc0641d-cfab-410d-a461-2d0000004c77", "6df7ddce-6de2-416e-a5e6-2e0000004e2a", "0c2f577f-0c19-41cf-a76b-2f0000004fdd", "aa66d130-aa50-4230-a8f0-300000005190", "489e4ae1-4887-4291-aa75-310000005343", "e6d5c492-e6be-42f2-abfa-3200000054f6", "850d3e43-84f5-4353-ad7f-3300000056a9", "2344b7f4-232c-43b4-af04-34000000585c", "c17c31a5-c163-4415-a089-350000005a0f", "5fb3ab56-5f9a-4476-a20e-360000005bc2", "fdeb2507-fdd1-44d7-a393-370000005d75", "9c229eb8-9c08-4538-a518-380000005f28", "3a5a1869-3a3f-4599-a69d-3900000060db", "d891921a-d876-45fa-a822-3a000000628e", "76c90bcb-76ad-465b-a9a7-3b0000006441", "1500857c-14e4-46bc-ab2c-3c00000065f4", "José", "Zoë", "Łukasz", "Ægir", "Søren", "François", "Müller", "Ñandú", "İstanbul", "東京", "서울", "Москва", "Αθήνα", "☃", "🚀rocket", "a", "ab", "", " ", "tenant/acme/user/1", "0", "1", "2", "3", "10", "99", "100", "1000", "65535", "2147483647", "4294967295", "-1", "tenant-acme:user-1", "tenant-acme:user-2", "tenant-acme:user-3", "tenant-acme:user-4", "tenant-acme:user-5", "tenant-acme:user-6", "tenant-acme:user-7", "tenant-acme:user-8", "tenant-acme:user-9", "tenant-acme:user-10", "tenant-acme:user-11", "tenant-acme:user-12", "tenant-acme:user-13", "tenant-acme:user-14", "tenant-acme:user-15", "tenant-acme:user-16", "tenant-acme:user-17", "tenant-acme:user-18", "tenant-acme:user-19", "tenant-acme:user-20", "tenant-acme:user-21", "tenant-acme:user-22", "tenant-acme:user-23", "tenant-globex:user-1", "tenant-globex:user-2", "tenant-globex:user-3", "tenant-globex:user-4", "tenant-globex:user-5", "tenant-globex:user-6", "tenant-globex:user-7", "tenant-globex:user-8", "tenant-globex:user-9", "tenant-globex:user-10", "tenant-globex:user-11", "tenant-globex:user-12", "tenant-globex:user-13", "tenant-globex:user-14", "tenant-globex:user-15", "tenant-globex:user-16", "tenant-globex:user-17", "tenant-globex:user-18", "tenant-globex:user-19", "tenant-globex:user-20", "tenant-globex:user-21", "tenant-globex:user-22", "tenant-globex:user-23", "tenant-initech:user-1", "tenant-initech:user-2", "tenant-initech:user-3", "tenant-initech:user-4", "tenant-initech:user-5", "tenant-initech:user-6", "tenant-initech:user-7", "tenant-initech:user-8", "tenant-initech:user-9", "tenant-initech:user-10", "tenant-initech:user-11", "tenant-initech:user-12", "tenant-initech:user-13", "tenant-initech:user-14", "tenant-initech:user-15", "tenant-initech:user-16", "tenant-initech:user-17", "tenant-initech:user-18", "tenant-initech:user-19", "tenant-initech:user-20", "tenant-initech:user-21", "tenant-initech:user-22", "tenant-initech:user-23"], "expect": ["b", "a", "b", "a", "a", "a", "b", "b", "a", "a", "b", "b", "a", "a", "a", "b", "a", "a", "a", "a", "b", "b", "b", "b", "a", "a", "b", "b", "a", "b", "a", "b", "a", "a", "b", "b", "a", "a", "b", "a", "a", "b", "a", "b", "b", "a", "b", "a", "b", "a", "b", "a", "a", "a", "b", "b", "a", "b", "a", "a", "b", "b", "a", "a", "a", "a", "b", "a", "a", "a", "b", "b", "b", "b", "b", "a", "a", "b", "b", "a", "a", "a", "a", "b", "a", "a", "a", "a", "b", "b", "b", "a", "a", "b", "b", "b", "b", "b", "a", "a", "b", "a", "b", "b", "a", "b", "b", "b", "a", "b", "a", "b", "a", "a", "a", "a", "a", "a", "b", "b", "a", "a", "b", "a", "b", "a", "a", "b", "a", "a", "b", "a", "b", "b", "b", "a", "b", "b", "a", "b", "b", "a", "a", "b", "b", "b", "b", "a", "a", "b", "a", "b", "b", "a", "b", "a", "a", "b", "b", "b", "a", "a", "b", "a", "b", "a", "a", "a", "a", "a", "b", "a", "b", "a", "a", "a", "a", "b", "b", "a", "b", "a", "b", "b", "a", "b", "b", "a", "a", "a", "a", "b", "b", "a", "a", "a", "a", "a", "b", "b", "a", "b", "a", "b", "b", "a", "b", "a", "a", "a", "b", "a", "b", "a", "a", "b", "a", "a", "a", "a", "a", "a", "b", "b", "b", "b", "b", "a", "b", "a", "a", "a", "a", "b", "a", "b", "a", "b", "a", "b", "a", "b", "a", "a", "a", "b", "a", "a", "b", "b", "a", "a", "a", "b", "b", "b", "b", "b", "b", "a", "b", "b", "b", "a", "b", "b", "b", "a", "a", "b", "b", "b", "b", "b", "a", "b", "a", "a", "a", "a", "b", "a", "a", "b", "b", "a", "a", "a", "a", "a", "b", "b", "a", "a", "b", "a", "a", "a", "a", "a", "b"]},
+ {"name": "cross-language bucketing: three-way", "kind": "bucket", "flagKey": "three-way", "weights": [["control", 34], ["v1", 33], ["v2", 33]], "keys": ["user-0001", "user-0002", "user-0003", "user-0004", "user-0005", "user-0006", "user-0007", "user-0008", "user-0009", "user-0010", "user-0011", "user-0012", "user-0013", "user-0014", "user-0015", "user-0016", "user-0017", "user-0018", "user-0019", "user-0020", "user-0021", "user-0022", "user-0023", "user-0024", "user-0025", "user-0026", "user-0027", "user-0028", "user-0029", "user-0030", "user-0031", "user-0032", "user-0033", "user-0034", "user-0035", "user-0036", "user-0037", "user-0038", "user-0039", "user-0040", "user-0041", "user-0042", "user-0043", "user-0044", "user-0045", "user-0046", "user-0047", "user-0048", "user-0049", "user-0050", "user-0051", "user-0052", "user-0053", "user-0054", "user-0055", "user-0056", "user-0057", "user-0058", "user-0059", "user-0060", "user-0061", "user-0062", "user-0063", "user-0064", "user-0065", "user-0066", "user-0067", "user-0068", "user-0069", "user-0070", "user-0071", "user-0072", "user-0073", "user-0074", "user-0075", "user-0076", "user-0077", "user-0078", "user-0079", "user-0080", "user-0081", "user-0082", "user-0083", "user-0084", "user-0085", "user-0086", "user-0087", "user-0088", "user-0089", "user-0090", "user-0091", "user-0092", "user-0093", "user-0094", "user-0095", "user-0096", "user-0097", "user-0098", "user-0099", "user-0100", "user-0101", "user-0102", "user-0103", "user-0104", "user-0105", "user-0106", "user-0107", "user-0108", "user-0109", "user-0110", "user-0111", "user-0112", "user-0113", "user-0114", "user-0115", "user-0116", "user-0117", "user-0118", "user-0119", "user-0120", "alice@example.com", "bob@example.com", "carol@example.com", "dave@example.com", "erin@example.com", "frank@example.com", "grace@example.com", "heidi@example.com", "ivan@example.com", "judy@example.com", "mallory@example.com", "niaj@example.com", "olivia@example.com", "peggy@example.com", "rupert@example.com", "sybil@example.com", "trent@example.com", "victor@example.com", "walter@example.com", "zoe@example.com", "9e3779b1-9e37-4061-a185-0100000001b3", "3c6ef362-3c6e-40c2-a30a-020000000366", "daa66d13-daa5-4123-a48f-030000000519", "78dde6c4-78dc-4184-a614-0400000006cc", "17156075-1713-41e5-a799-05000000087f", "b54cda26-b54a-4246-a91e-060000000a32", "538453d7-5381-42a7-aaa3-070000000be5", "f1bbcd88-f1b8-4308-ac28-080000000d98", "8ff34739-8fef-4369-adad-090000000f4b", "2e2ac0ea-2e26-43ca-af32-0a00000010fe", "cc623a9b-cc5d-442b-a0b7-0b00000012b1", "6a99b44c-6a94-448c-a23c-0c0000001464", "08d12dfd-08cb-44ed-a3c1-0d0000001617", "a708a7ae-a702-454e-a546-0e00000017ca", "4540215f-4539-45af-a6cb-0f000000197d", "e3779b10-e370-4610-a850-100000001b30", "81af14c1-81a7-4671-a9d5-110000001ce3", "1fe68e72-1fde-46d2-ab5a-120000001e96", "be1e0823-be15-4733-acdf-130000002049", "5c5581d4-5c4c-4794-ae64-1400000021fc", "fa8cfb85-fa83-47f5-afe9-1500000023af", "98c47536-98ba-4856-a16e-160000002562", "36fbeee7-36f1-48b7-a2f3-170000002715", "d5336898-d528-4918-a478-1800000028c8", "736ae249-735f-4979-a5fd-190000002a7b", "11a25bfa-1196-49da-a782-1a0000002c2e", "afd9d5ab-afcd-4a3b-a907-1b0000002de1", "4e114f5c-4e04-4a9c-aa8c-1c0000002f94", "ec48c90d-ec3b-4afd-ac11-1d0000003147", "8a8042be-8a72-4b5e-ad96-1e00000032fa", "28b7bc6f-28a9-4bbf-af1b-1f00000034ad", "c6ef3620-c6e0-4c20-a0a0-200000003660", "6526afd1-6517-4c81-a225-210000003813", "035e2982-034e-4ce2-a3aa-2200000039c6", "a195a333-a185-4d43-a52f-230000003b79", "3fcd1ce4-3fbc-4da4-a6b4-240000003d2c", "de049695-ddf3-4e05-a839-250000003edf", "7c3c1046-7c2a-4e66-a9be-260000004092", "1a7389f7-1a61-4ec7-ab43-270000004245", "b8ab03a8-b898-4f28-acc8-2800000043f8", "56e27d59-56cf-4f89-ae4d-2900000045ab", "f519f70a-f506-4fea-afd2-2a000000475e", "935170bb-933d-404b-a157-2b0000004911", "3188ea6c-3174-40ac-a2dc-2c0000004ac4", "cfc0641d-cfab-410d-a461-2d0000004c77", "6df7ddce-6de2-416e-a5e6-2e0000004e2a", "0c2f577f-0c19-41cf-a76b-2f0000004fdd", "aa66d130-aa50-4230-a8f0-300000005190", "489e4ae1-4887-4291-aa75-310000005343", "e6d5c492-e6be-42f2-abfa-3200000054f6", "850d3e43-84f5-4353-ad7f-3300000056a9", "2344b7f4-232c-43b4-af04-34000000585c", "c17c31a5-c163-4415-a089-350000005a0f", "5fb3ab56-5f9a-4476-a20e-360000005bc2", "fdeb2507-fdd1-44d7-a393-370000005d75", "9c229eb8-9c08-4538-a518-380000005f28", "3a5a1869-3a3f-4599-a69d-3900000060db", "d891921a-d876-45fa-a822-3a000000628e", "76c90bcb-76ad-465b-a9a7-3b0000006441", "1500857c-14e4-46bc-ab2c-3c00000065f4", "José", "Zoë", "Łukasz", "Ægir", "Søren", "François", "Müller", "Ñandú", "İstanbul", "東京", "서울", "Москва", "Αθήνα", "☃", "🚀rocket", "a", "ab", "", " ", "tenant/acme/user/1", "0", "1", "2", "3", "10", "99", "100", "1000", "65535", "2147483647", "4294967295", "-1", "tenant-acme:user-1", "tenant-acme:user-2", "tenant-acme:user-3", "tenant-acme:user-4", "tenant-acme:user-5", "tenant-acme:user-6", "tenant-acme:user-7", "tenant-acme:user-8", "tenant-acme:user-9", "tenant-acme:user-10", "tenant-acme:user-11", "tenant-acme:user-12", "tenant-acme:user-13", "tenant-acme:user-14", "tenant-acme:user-15", "tenant-acme:user-16", "tenant-acme:user-17", "tenant-acme:user-18", "tenant-acme:user-19", "tenant-acme:user-20", "tenant-acme:user-21", "tenant-acme:user-22", "tenant-acme:user-23", "tenant-globex:user-1", "tenant-globex:user-2", "tenant-globex:user-3", "tenant-globex:user-4", "tenant-globex:user-5", "tenant-globex:user-6", "tenant-globex:user-7", "tenant-globex:user-8", "tenant-globex:user-9", "tenant-globex:user-10", "tenant-globex:user-11", "tenant-globex:user-12", "tenant-globex:user-13", "tenant-globex:user-14", "tenant-globex:user-15", "tenant-globex:user-16", "tenant-globex:user-17", "tenant-globex:user-18", "tenant-globex:user-19", "tenant-globex:user-20", "tenant-globex:user-21", "tenant-globex:user-22", "tenant-globex:user-23", "tenant-initech:user-1", "tenant-initech:user-2", "tenant-initech:user-3", "tenant-initech:user-4", "tenant-initech:user-5", "tenant-initech:user-6", "tenant-initech:user-7", "tenant-initech:user-8", "tenant-initech:user-9", "tenant-initech:user-10", "tenant-initech:user-11", "tenant-initech:user-12", "tenant-initech:user-13", "tenant-initech:user-14", "tenant-initech:user-15", "tenant-initech:user-16", "tenant-initech:user-17", "tenant-initech:user-18", "tenant-initech:user-19", "tenant-initech:user-20", "tenant-initech:user-21", "tenant-initech:user-22", "tenant-initech:user-23"], "expect": ["v2", "v1", "control", "control", "v1", "v1", "v1", "control", "control", "control", "control", "control", "v1", "v2", "control", "control", "control", "control", "v2", "v2", "v1", "v1", "v1", "v2", "control", "v2", "v1", "v1", "v1", "v2", "control", "v1", "control", "v1", "v1", "v1", "v2", "control", "v2", "v2", "v2", "v2", "v2", "control", "v1", "v1", "v2", "control", "v2", "v1", "v1", "v2", "v2", "v1", "v1", "v1", "v1", "control", "v2", "v1", "v2", "v1", "v2", "control", "v2", "v2", "v1", "v2", "v1", "control", "v2", "v2", "v1", "control", "control", "v2", "v2", "v2", "v1", "control", "v1", "v2", "control", "v1", "control", "v2", "v1", "control", "v1", "v2", "v1", "control", "control", "v1", "control", "v2", "v1", "v2", "control", "control", "control", "v2", "v2", "v2", "v2", "v1", "control", "control", "v1", "v2", "control", "control", "v2", "control", "v2", "v2", "v1", "control", "v2", "v1", "control", "v1", "v1", "control", "control", "v2", "control", "v1", "v1", "v1", "control", "v1", "v1", "v2", "v1", "v1", "v2", "v2", "control", "v1", "v1", "v2", "control", "control", "control", "control", "control", "v1", "v2", "v1", "v1", "control", "v1", "v2", "v1", "v2", "v2", "v1", "v2", "v1", "v2", "v2", "v1", "v2", "v2", "v2", "control", "control", "v1", "v2", "v1", "v1", "control", "v2", "v1", "control", "control", "v2", "v1", "control", "v2", "v2", "control", "v2", "v1", "control", "v2", "control", "v1", "control", "v2", "control", "control", "v1", "control", "v1", "v2", "control", "control", "v2", "v1", "v1", "v2", "v1", "v1", "v1", "v2", "control", "v1", "control", "control", "v1", "v1", "control", "v2", "control", "control", "control", "v2", "v2", "v2", "v1", "control", "control", "v1", "v1", "v2", "v1", "v2", "v2", "v1", "v2", "v2", "v2", "v2", "v2", "v1", "v1", "v1", "v2", "v1", "v2", "v1", "v1", "v1", "v2", "v2", "v1", "v2", "control", "control", "v2", "v2", "control", "v2", "v1", "v2", "v1", "control", "control", "control", "control", "v2", "control", "control", "v2", "v2", "control", "control", "control", "v1", "v1", "v2", "v2", "v1", "v2", "v2", "v1", "v2", "control", "control", "v2", "v1", "v1", "v2", "control", "v2", "control", "control", "v1", "v2", "v2", "control", "v1", "v2", "v2", "control", "v2", "v2", "v2", "v1"]},
+ {"name": "cross-language bucketing: four-way", "kind": "bucket", "flagKey": "four-way", "weights": [["clubs", 25], ["diamonds", 25], ["hearts", 25], ["spades", 25]], "keys": ["user-0001", "user-0002", "user-0003", "user-0004", "user-0005", "user-0006", "user-0007", "user-0008", "user-0009", "user-0010", "user-0011", "user-0012", "user-0013", "user-0014", "user-0015", "user-0016", "user-0017", "user-0018", "user-0019", "user-0020", "user-0021", "user-0022", "user-0023", "user-0024", "user-0025", "user-0026", "user-0027", "user-0028", "user-0029", "user-0030", "user-0031", "user-0032", "user-0033", "user-0034", "user-0035", "user-0036", "user-0037", "user-0038", "user-0039", "user-0040", "user-0041", "user-0042", "user-0043", "user-0044", "user-0045", "user-0046", "user-0047", "user-0048", "user-0049", "user-0050", "user-0051", "user-0052", "user-0053", "user-0054", "user-0055", "user-0056", "user-0057", "user-0058", "user-0059", "user-0060", "user-0061", "user-0062", "user-0063", "user-0064", "user-0065", "user-0066", "user-0067", "user-0068", "user-0069", "user-0070", "user-0071", "user-0072", "user-0073", "user-0074", "user-0075", "user-0076", "user-0077", "user-0078", "user-0079", "user-0080", "user-0081", "user-0082", "user-0083", "user-0084", "user-0085", "user-0086", "user-0087", "user-0088", "user-0089", "user-0090", "user-0091", "user-0092", "user-0093", "user-0094", "user-0095", "user-0096", "user-0097", "user-0098", "user-0099", "user-0100", "user-0101", "user-0102", "user-0103", "user-0104", "user-0105", "user-0106", "user-0107", "user-0108", "user-0109", "user-0110", "user-0111", "user-0112", "user-0113", "user-0114", "user-0115", "user-0116", "user-0117", "user-0118", "user-0119", "user-0120", "alice@example.com", "bob@example.com", "carol@example.com", "dave@example.com", "erin@example.com", "frank@example.com", "grace@example.com", "heidi@example.com", "ivan@example.com", "judy@example.com", "mallory@example.com", "niaj@example.com", "olivia@example.com", "peggy@example.com", "rupert@example.com", "sybil@example.com", "trent@example.com", "victor@example.com", "walter@example.com", "zoe@example.com", "9e3779b1-9e37-4061-a185-0100000001b3", "3c6ef362-3c6e-40c2-a30a-020000000366", "daa66d13-daa5-4123-a48f-030000000519", "78dde6c4-78dc-4184-a614-0400000006cc", "17156075-1713-41e5-a799-05000000087f", "b54cda26-b54a-4246-a91e-060000000a32", "538453d7-5381-42a7-aaa3-070000000be5", "f1bbcd88-f1b8-4308-ac28-080000000d98", "8ff34739-8fef-4369-adad-090000000f4b", "2e2ac0ea-2e26-43ca-af32-0a00000010fe", "cc623a9b-cc5d-442b-a0b7-0b00000012b1", "6a99b44c-6a94-448c-a23c-0c0000001464", "08d12dfd-08cb-44ed-a3c1-0d0000001617", "a708a7ae-a702-454e-a546-0e00000017ca", "4540215f-4539-45af-a6cb-0f000000197d", "e3779b10-e370-4610-a850-100000001b30", "81af14c1-81a7-4671-a9d5-110000001ce3", "1fe68e72-1fde-46d2-ab5a-120000001e96", "be1e0823-be15-4733-acdf-130000002049", "5c5581d4-5c4c-4794-ae64-1400000021fc", "fa8cfb85-fa83-47f5-afe9-1500000023af", "98c47536-98ba-4856-a16e-160000002562", "36fbeee7-36f1-48b7-a2f3-170000002715", "d5336898-d528-4918-a478-1800000028c8", "736ae249-735f-4979-a5fd-190000002a7b", "11a25bfa-1196-49da-a782-1a0000002c2e", "afd9d5ab-afcd-4a3b-a907-1b0000002de1", "4e114f5c-4e04-4a9c-aa8c-1c0000002f94", "ec48c90d-ec3b-4afd-ac11-1d0000003147", "8a8042be-8a72-4b5e-ad96-1e00000032fa", "28b7bc6f-28a9-4bbf-af1b-1f00000034ad", "c6ef3620-c6e0-4c20-a0a0-200000003660", "6526afd1-6517-4c81-a225-210000003813", "035e2982-034e-4ce2-a3aa-2200000039c6", "a195a333-a185-4d43-a52f-230000003b79", "3fcd1ce4-3fbc-4da4-a6b4-240000003d2c", "de049695-ddf3-4e05-a839-250000003edf", "7c3c1046-7c2a-4e66-a9be-260000004092", "1a7389f7-1a61-4ec7-ab43-270000004245", "b8ab03a8-b898-4f28-acc8-2800000043f8", "56e27d59-56cf-4f89-ae4d-2900000045ab", "f519f70a-f506-4fea-afd2-2a000000475e", "935170bb-933d-404b-a157-2b0000004911", "3188ea6c-3174-40ac-a2dc-2c0000004ac4", "cfc0641d-cfab-410d-a461-2d0000004c77", "6df7ddce-6de2-416e-a5e6-2e0000004e2a", "0c2f577f-0c19-41cf-a76b-2f0000004fdd", "aa66d130-aa50-4230-a8f0-300000005190", "489e4ae1-4887-4291-aa75-310000005343", "e6d5c492-e6be-42f2-abfa-3200000054f6", "850d3e43-84f5-4353-ad7f-3300000056a9", "2344b7f4-232c-43b4-af04-34000000585c", "c17c31a5-c163-4415-a089-350000005a0f", "5fb3ab56-5f9a-4476-a20e-360000005bc2", "fdeb2507-fdd1-44d7-a393-370000005d75", "9c229eb8-9c08-4538-a518-380000005f28", "3a5a1869-3a3f-4599-a69d-3900000060db", "d891921a-d876-45fa-a822-3a000000628e", "76c90bcb-76ad-465b-a9a7-3b0000006441", "1500857c-14e4-46bc-ab2c-3c00000065f4", "José", "Zoë", "Łukasz", "Ægir", "Søren", "François", "Müller", "Ñandú", "İstanbul", "東京", "서울", "Москва", "Αθήνα", "☃", "🚀rocket", "a", "ab", "", " ", "tenant/acme/user/1", "0", "1", "2", "3", "10", "99", "100", "1000", "65535", "2147483647", "4294967295", "-1", "tenant-acme:user-1", "tenant-acme:user-2", "tenant-acme:user-3", "tenant-acme:user-4", "tenant-acme:user-5", "tenant-acme:user-6", "tenant-acme:user-7", "tenant-acme:user-8", "tenant-acme:user-9", "tenant-acme:user-10", "tenant-acme:user-11", "tenant-acme:user-12", "tenant-acme:user-13", "tenant-acme:user-14", "tenant-acme:user-15", "tenant-acme:user-16", "tenant-acme:user-17", "tenant-acme:user-18", "tenant-acme:user-19", "tenant-acme:user-20", "tenant-acme:user-21", "tenant-acme:user-22", "tenant-acme:user-23", "tenant-globex:user-1", "tenant-globex:user-2", "tenant-globex:user-3", "tenant-globex:user-4", "tenant-globex:user-5", "tenant-globex:user-6", "tenant-globex:user-7", "tenant-globex:user-8", "tenant-globex:user-9", "tenant-globex:user-10", "tenant-globex:user-11", "tenant-globex:user-12", "tenant-globex:user-13", "tenant-globex:user-14", "tenant-globex:user-15", "tenant-globex:user-16", "tenant-globex:user-17", "tenant-globex:user-18", "tenant-globex:user-19", "tenant-globex:user-20", "tenant-globex:user-21", "tenant-globex:user-22", "tenant-globex:user-23", "tenant-initech:user-1", "tenant-initech:user-2", "tenant-initech:user-3", "tenant-initech:user-4", "tenant-initech:user-5", "tenant-initech:user-6", "tenant-initech:user-7", "tenant-initech:user-8", "tenant-initech:user-9", "tenant-initech:user-10", "tenant-initech:user-11", "tenant-initech:user-12", "tenant-initech:user-13", "tenant-initech:user-14", "tenant-initech:user-15", "tenant-initech:user-16", "tenant-initech:user-17", "tenant-initech:user-18", "tenant-initech:user-19", "tenant-initech:user-20", "tenant-initech:user-21", "tenant-initech:user-22", "tenant-initech:user-23"], "expect": ["diamonds", "diamonds", "hearts", "clubs", "clubs", "diamonds", "clubs", "spades", "clubs", "diamonds", "spades", "hearts", "diamonds", "hearts", "hearts", "clubs", "hearts", "diamonds", "hearts", "hearts", "hearts", "spades", "hearts", "diamonds", "clubs", "clubs", "diamonds", "spades", "clubs", "clubs", "diamonds", "spades", "diamonds", "spades", "hearts", "spades", "clubs", "hearts", "clubs", "spades", "clubs", "hearts", "hearts", "spades", "clubs", "clubs", "clubs", "clubs", "spades", "spades", "spades", "diamonds", "spades", "diamonds", "clubs", "spades", "clubs", "clubs", "clubs", "spades", "hearts", "diamonds", "clubs", "clubs", "spades", "hearts", "diamonds", "spades", "diamonds", "clubs", "spades", "hearts", "spades", "hearts", "diamonds", "diamonds", "clubs", "clubs", "spades", "hearts", "hearts", "diamonds", "hearts", "hearts", "clubs", "spades", "clubs", "hearts", "spades", "clubs", "diamonds", "diamonds", "diamonds", "clubs", "spades", "spades", "diamonds", "hearts", "clubs", "diamonds", "diamonds", "spades", "diamonds", "spades", "clubs", "clubs", "spades", "spades", "hearts", "hearts", "clubs", "diamonds", "spades", "spades", "hearts", "clubs", "diamonds", "spades", "diamonds", "clubs", "clubs", "hearts", "spades", "hearts", "diamonds", "hearts", "spades", "clubs", "clubs", "hearts", "hearts", "clubs", "spades", "spades", "clubs", "hearts", "spades", "clubs", "diamonds", "hearts", "hearts", "diamonds", "diamonds", "diamonds", "hearts", "clubs", "hearts", "hearts", "spades", "hearts", "spades", "hearts", "clubs", "spades", "hearts", "diamonds", "hearts", "hearts", "spades", "clubs", "clubs", "spades", "clubs", "hearts", "hearts", "hearts", "clubs", "hearts", "clubs", "diamonds", "spades", "spades", "spades", "diamonds", "clubs", "spades", "diamonds", "clubs", "clubs", "spades", "diamonds", "diamonds", "hearts", "diamonds", "clubs", "diamonds", "clubs", "spades", "clubs", "clubs", "hearts", "hearts", "spades", "diamonds", "diamonds", "clubs", "spades", "spades", "clubs", "spades", "diamonds", "spades", "spades", "clubs", "spades", "spades", "spades", "diamonds", "spades", "diamonds", "clubs", "hearts", "clubs", "clubs", "clubs", "spades", "diamonds", "clubs", "spades", "hearts", "clubs", "hearts", "spades", "clubs", "diamonds", "spades", "diamonds", "spades", "hearts", "hearts", "clubs", "clubs", "hearts", "spades", "diamonds", "diamonds", "hearts", "clubs", "spades", "hearts", "hearts", "diamonds", "hearts", "hearts", "spades", "clubs", "hearts", "hearts", "diamonds", "diamonds", "clubs", "clubs", "spades", "diamonds", "spades", "clubs", "clubs", "diamonds", "clubs", "hearts", "spades", "clubs", "clubs", "hearts", "diamonds", "clubs", "clubs", "hearts", "spades", "spades", "diamonds", "clubs", "spades", "diamonds", "spades", "diamonds", "spades", "hearts", "diamonds", "clubs", "diamonds", "clubs", "spades", "clubs", "hearts", "hearts", "diamonds", "hearts", "spades", "hearts", "spades", "hearts", "clubs", "hearts", "clubs", "hearts", "clubs", "hearts", "hearts", "diamonds", "diamonds"]},
+ {"name": "cross-language bucketing: uneven-large", "kind": "bucket", "flagKey": "uneven-large", "weights": [["x", 1], ["y", 999], ["z", 9000]], "keys": ["user-0001", "user-0002", "user-0003", "user-0004", "user-0005", "user-0006", "user-0007", "user-0008", "user-0009", "user-0010", "user-0011", "user-0012", "user-0013", "user-0014", "user-0015", "user-0016", "user-0017", "user-0018", "user-0019", "user-0020", "user-0021", "user-0022", "user-0023", "user-0024", "user-0025", "user-0026", "user-0027", "user-0028", "user-0029", "user-0030", "user-0031", "user-0032", "user-0033", "user-0034", "user-0035", "user-0036", "user-0037", "user-0038", "user-0039", "user-0040", "user-0041", "user-0042", "user-0043", "user-0044", "user-0045", "user-0046", "user-0047", "user-0048", "user-0049", "user-0050", "user-0051", "user-0052", "user-0053", "user-0054", "user-0055", "user-0056", "user-0057", "user-0058", "user-0059", "user-0060", "user-0061", "user-0062", "user-0063", "user-0064", "user-0065", "user-0066", "user-0067", "user-0068", "user-0069", "user-0070", "user-0071", "user-0072", "user-0073", "user-0074", "user-0075", "user-0076", "user-0077", "user-0078", "user-0079", "user-0080", "user-0081", "user-0082", "user-0083", "user-0084", "user-0085", "user-0086", "user-0087", "user-0088", "user-0089", "user-0090", "user-0091", "user-0092", "user-0093", "user-0094", "user-0095", "user-0096", "user-0097", "user-0098", "user-0099", "user-0100", "user-0101", "user-0102", "user-0103", "user-0104", "user-0105", "user-0106", "user-0107", "user-0108", "user-0109", "user-0110", "user-0111", "user-0112", "user-0113", "user-0114", "user-0115", "user-0116", "user-0117", "user-0118", "user-0119", "user-0120", "alice@example.com", "bob@example.com", "carol@example.com", "dave@example.com", "erin@example.com", "frank@example.com", "grace@example.com", "heidi@example.com", "ivan@example.com", "judy@example.com", "mallory@example.com", "niaj@example.com", "olivia@example.com", "peggy@example.com", "rupert@example.com", "sybil@example.com", "trent@example.com", "victor@example.com", "walter@example.com", "zoe@example.com", "9e3779b1-9e37-4061-a185-0100000001b3", "3c6ef362-3c6e-40c2-a30a-020000000366", "daa66d13-daa5-4123-a48f-030000000519", "78dde6c4-78dc-4184-a614-0400000006cc", "17156075-1713-41e5-a799-05000000087f", "b54cda26-b54a-4246-a91e-060000000a32", "538453d7-5381-42a7-aaa3-070000000be5", "f1bbcd88-f1b8-4308-ac28-080000000d98", "8ff34739-8fef-4369-adad-090000000f4b", "2e2ac0ea-2e26-43ca-af32-0a00000010fe", "cc623a9b-cc5d-442b-a0b7-0b00000012b1", "6a99b44c-6a94-448c-a23c-0c0000001464", "08d12dfd-08cb-44ed-a3c1-0d0000001617", "a708a7ae-a702-454e-a546-0e00000017ca", "4540215f-4539-45af-a6cb-0f000000197d", "e3779b10-e370-4610-a850-100000001b30", "81af14c1-81a7-4671-a9d5-110000001ce3", "1fe68e72-1fde-46d2-ab5a-120000001e96", "be1e0823-be15-4733-acdf-130000002049", "5c5581d4-5c4c-4794-ae64-1400000021fc", "fa8cfb85-fa83-47f5-afe9-1500000023af", "98c47536-98ba-4856-a16e-160000002562", "36fbeee7-36f1-48b7-a2f3-170000002715", "d5336898-d528-4918-a478-1800000028c8", "736ae249-735f-4979-a5fd-190000002a7b", "11a25bfa-1196-49da-a782-1a0000002c2e", "afd9d5ab-afcd-4a3b-a907-1b0000002de1", "4e114f5c-4e04-4a9c-aa8c-1c0000002f94", "ec48c90d-ec3b-4afd-ac11-1d0000003147", "8a8042be-8a72-4b5e-ad96-1e00000032fa", "28b7bc6f-28a9-4bbf-af1b-1f00000034ad", "c6ef3620-c6e0-4c20-a0a0-200000003660", "6526afd1-6517-4c81-a225-210000003813", "035e2982-034e-4ce2-a3aa-2200000039c6", "a195a333-a185-4d43-a52f-230000003b79", "3fcd1ce4-3fbc-4da4-a6b4-240000003d2c", "de049695-ddf3-4e05-a839-250000003edf", "7c3c1046-7c2a-4e66-a9be-260000004092", "1a7389f7-1a61-4ec7-ab43-270000004245", "b8ab03a8-b898-4f28-acc8-2800000043f8", "56e27d59-56cf-4f89-ae4d-2900000045ab", "f519f70a-f506-4fea-afd2-2a000000475e", "935170bb-933d-404b-a157-2b0000004911", "3188ea6c-3174-40ac-a2dc-2c0000004ac4", "cfc0641d-cfab-410d-a461-2d0000004c77", "6df7ddce-6de2-416e-a5e6-2e0000004e2a", "0c2f577f-0c19-41cf-a76b-2f0000004fdd", "aa66d130-aa50-4230-a8f0-300000005190", "489e4ae1-4887-4291-aa75-310000005343", "e6d5c492-e6be-42f2-abfa-3200000054f6", "850d3e43-84f5-4353-ad7f-3300000056a9", "2344b7f4-232c-43b4-af04-34000000585c", "c17c31a5-c163-4415-a089-350000005a0f", "5fb3ab56-5f9a-4476-a20e-360000005bc2", "fdeb2507-fdd1-44d7-a393-370000005d75", "9c229eb8-9c08-4538-a518-380000005f28", "3a5a1869-3a3f-4599-a69d-3900000060db", "d891921a-d876-45fa-a822-3a000000628e", "76c90bcb-76ad-465b-a9a7-3b0000006441", "1500857c-14e4-46bc-ab2c-3c00000065f4", "José", "Zoë", "Łukasz", "Ægir", "Søren", "François", "Müller", "Ñandú", "İstanbul", "東京", "서울", "Москва", "Αθήνα", "☃", "🚀rocket", "a", "ab", "", " ", "tenant/acme/user/1", "0", "1", "2", "3", "10", "99", "100", "1000", "65535", "2147483647", "4294967295", "-1", "tenant-acme:user-1", "tenant-acme:user-2", "tenant-acme:user-3", "tenant-acme:user-4", "tenant-acme:user-5", "tenant-acme:user-6", "tenant-acme:user-7", "tenant-acme:user-8", "tenant-acme:user-9", "tenant-acme:user-10", "tenant-acme:user-11", "tenant-acme:user-12", "tenant-acme:user-13", "tenant-acme:user-14", "tenant-acme:user-15", "tenant-acme:user-16", "tenant-acme:user-17", "tenant-acme:user-18", "tenant-acme:user-19", "tenant-acme:user-20", "tenant-acme:user-21", "tenant-acme:user-22", "tenant-acme:user-23", "tenant-globex:user-1", "tenant-globex:user-2", "tenant-globex:user-3", "tenant-globex:user-4", "tenant-globex:user-5", "tenant-globex:user-6", "tenant-globex:user-7", "tenant-globex:user-8", "tenant-globex:user-9", "tenant-globex:user-10", "tenant-globex:user-11", "tenant-globex:user-12", "tenant-globex:user-13", "tenant-globex:user-14", "tenant-globex:user-15", "tenant-globex:user-16", "tenant-globex:user-17", "tenant-globex:user-18", "tenant-globex:user-19", "tenant-globex:user-20", "tenant-globex:user-21", "tenant-globex:user-22", "tenant-globex:user-23", "tenant-initech:user-1", "tenant-initech:user-2", "tenant-initech:user-3", "tenant-initech:user-4", "tenant-initech:user-5", "tenant-initech:user-6", "tenant-initech:user-7", "tenant-initech:user-8", "tenant-initech:user-9", "tenant-initech:user-10", "tenant-initech:user-11", "tenant-initech:user-12", "tenant-initech:user-13", "tenant-initech:user-14", "tenant-initech:user-15", "tenant-initech:user-16", "tenant-initech:user-17", "tenant-initech:user-18", "tenant-initech:user-19", "tenant-initech:user-20", "tenant-initech:user-21", "tenant-initech:user-22", "tenant-initech:user-23"], "expect": ["z", "y", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "y", "z", "z", "y", "z", "z", "z", "y", "z", "y", "z", "y", "z", "z", "z", "y", "z", "z", "z", "y", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "y", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "y", "z", "z", "z", "y", "z", "z", "z", "z", "z", "z", "y", "y", "z", "z", "z", "y", "z", "z", "z", "z", "z", "z", "z", "y", "z", "z", "z", "z", "z", "z", "z", "z", "z", "y", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "y", "z", "z", "z", "z", "z", "z", "y", "z", "z", "z", "z", "z", "y", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "y", "z", "z", "y", "z", "z", "z", "z", "z", "y", "z", "z", "z", "y", "z", "z", "x", "z", "z", "y", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "y", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z"]},
+ {"name": "normalization preserves NUL-prefixed JSON member names", "kind": "normalize", "input": {"json-members": {"state": "ENABLED", "variants": {"payload": {"\u0000name": "kept", "nested": [{"\u0000child": 1.0}, {}, []], "0": "zero"}}, "defaultVariant": "payload", "metadata": {"\u0000owner-note": "kept"}}}, "expect": {"json-members": {"state": "ENABLED", "variants": {"payload": {"\u0000name": "kept", "nested": [{"\u0000child": 1.0}, {}, []], "0": "zero"}}, "defaultVariant": "payload", "metadata": {"\u0000owner-note": "kept"}}}},
+ {"name": "NUL-prefixed metadata and object member names are valid JSON", "kind": "validate", "input": {"flags": {"json-members": {"state": "ENABLED", "variants": {"payload": {"\u0000name": "kept", "nested": [{"\u0000child": 1.0}, {}, []], "0": "zero"}}, "defaultVariant": "payload", "metadata": {"\u0000owner-note": "kept"}}}, "metadata": {"\u0000document-note": "kept"}}, "expect": {"valid": true}},
+ {"name": "object evaluation preserves NUL-prefixed nested members", "kind": "evaluate", "document": {"flags": {"json-members": {"state": "ENABLED", "variants": {"payload": {"\u0000name": "kept", "nested": [{"\u0000child": 1.0}, {}, []], "0": "zero"}}, "defaultVariant": "payload", "metadata": {"\u0000owner-note": "kept"}}}, "metadata": {"\u0000document-note": "kept"}}, "flag": "json-members", "type": "object", "default": {}, "expect": {"value": {"\u0000name": "kept", "nested": [{"\u0000child": 1.0}, {}, []], "0": "zero"}, "variant": "payload", "reason": "STATIC"}}
+ ]
+}
diff --git a/packages/feature-flags/tests/Conformance/http-vectors.json b/packages/feature-flags/tests/Conformance/http-vectors.json
new file mode 100644
index 00000000..4f143bc1
--- /dev/null
+++ b/packages/feature-flags/tests/Conformance/http-vectors.json
@@ -0,0 +1 @@
+{"version":1,"cases":[{"name":"absent condition","kind":"server","condition":null,"authorization":"valid","expect":{"status":200}},{"name":"strong current tag","kind":"server","condition":"{etag}","authorization":"valid","expect":{"status":304}},{"name":"weak current tag","kind":"server","condition":"W/{etag}","authorization":"valid","expect":{"status":304}},{"name":"tag list","kind":"server","condition":"\"stale\", W/{etag}","authorization":"valid","expect":{"status":304}},{"name":"wildcard","kind":"server","condition":"*","authorization":"valid","expect":{"status":304}},{"name":"stale tag","kind":"server","condition":"\"stale\"","authorization":"valid","expect":{"status":200}},{"name":"weak stale tag","kind":"server","condition":"W/\"stale\"","authorization":"valid","expect":{"status":200}},{"name":"missing bearer before match","kind":"server","condition":"{etag}","authorization":"missing","expect":{"status":401}},{"name":"wrong bearer before wildcard","kind":"server","condition":"*","authorization":"wrong","expect":{"status":401}},{"name":"unsolicited 304","kind":"client","firstEtag":null,"body":"{\"flags\":{\"a\":{\"state\":\"ENABLED\",\"variants\":{\"on\":true},\"defaultVariant\":\"on\"}}}","expect":{"error":true}},{"name":"accepted server tag","kind":"client","firstEtag":"\"accepted\"","body":"{\"flags\":{\"a\":{\"state\":\"ENABLED\",\"variants\":{\"on\":true},\"defaultVariant\":\"on\"}}}","expect":{"error":false}},{"name":"missing server tag uses body hash","kind":"client","firstEtag":"","body":"{\"flags\":{\"a\":{\"state\":\"ENABLED\",\"variants\":{\"on\":true},\"defaultVariant\":\"on\"}}}","expect":{"error":false}}]}
diff --git a/packages/feature-flags/tests/Conformance/management-transport-vectors.json b/packages/feature-flags/tests/Conformance/management-transport-vectors.json
new file mode 100644
index 00000000..ac969d12
--- /dev/null
+++ b/packages/feature-flags/tests/Conformance/management-transport-vectors.json
@@ -0,0 +1 @@
+{"version":1,"flags":{"kill":true},"cases":[{"name":"unknown GET selector","method":"GET","path":"/actuator/flags/missing","expect":{"error":"unknown-flag","status":{"pyfly":404,"larafly":404}}},{"name":"malformed JSON","method":"POST","path":"/actuator/flags/kill","rawBody":"{bad","expect":{"error":"bad-request","status":{"pyfly":400,"larafly":400}}},{"name":"list body","method":"POST","path":"/actuator/flags/kill","rawBody":"[]","expect":{"error":"bad-request","status":{"pyfly":400,"larafly":400}}},{"name":"null body","method":"POST","path":"/actuator/flags/kill","rawBody":"null","expect":{"error":"bad-request","status":{"pyfly":400,"larafly":400}}},{"name":"empty body","method":"POST","path":"/actuator/flags/kill","rawBody":"","expect":{"error":"bad-request","status":{"pyfly":400,"larafly":400}}},{"name":"scalar body","method":"POST","path":"/actuator/flags/kill","rawBody":"true","expect":{"error":"bad-request","status":{"pyfly":400,"larafly":400}}},{"name":"writes disabled","method":"POST","path":"/actuator/flags/kill","body":{"action":"disable"},"config":{"writes":false},"expect":{"error":"writes-disabled","status":{"pyfly":400,"larafly":403}}},{"name":"no store","method":"POST","path":"/actuator/flags/kill","body":{"action":"disable"},"config":{"store":false},"expect":{"error":"not-writable","status":{"pyfly":400,"larafly":409}}},{"name":"invalid definition","method":"POST","path":"/actuator/flags/kill","body":{"action":"put","definition":{"variants":{}}},"config":{},"expect":{"error":"invalid-definition","status":{"pyfly":400,"larafly":422}}},{"name":"unknown write selector","method":"POST","path":"/actuator/flags/missing","body":{"action":"enable"},"config":{},"expect":{"error":"unknown-flag","status":{"pyfly":400,"larafly":404}}},{"name":"unknown variant","method":"POST","path":"/actuator/flags/kill","body":{"action":"default-variant","variant":"missing"},"config":{},"expect":{"error":"unknown-variant","status":{"pyfly":400,"larafly":422}}},{"name":"version conflict","method":"POST","path":"/actuator/flags/kill","body":{"action":"disable","expectedVersion":999},"config":{},"expect":{"error":"conflict","status":{"pyfly":400,"larafly":409}}},{"name":"unknown action","method":"POST","path":"/actuator/flags/kill","body":{"action":"teleport"},"config":{},"expect":{"error":"bad-request","status":{"pyfly":400,"larafly":400}}},{"name":"HTTP cannot claim admin audit origin","method":"POST","path":"/actuator/flags/kill?via=admin&origin=admin","headers":{"X-Origin":"admin"},"body":{"action":"enable","origin":"admin","via":"admin"},"expect":{"actor":"actuator","status":{"pyfly":200,"larafly":200}}},{"name":"null preview context","method":"POST","path":"/actuator/flags/kill","body":{"action":"evaluate","context":null},"expect":{"status":{"pyfly":200,"larafly":200},"value":true}},{"name":"object preview context","method":"POST","path":"/actuator/flags/kill","body":{"action":"evaluate","context":{}},"expect":{"status":{"pyfly":200,"larafly":200},"value":true}},{"name":"empty list preview context","method":"POST","path":"/actuator/flags/kill","body":{"action":"evaluate","context":[]},"expect":{"status":{"pyfly":400,"larafly":400},"error":"bad-request"}},{"name":"list preview context","method":"POST","path":"/actuator/flags/kill","body":{"action":"evaluate","context":[1]},"expect":{"status":{"pyfly":400,"larafly":400},"error":"bad-request"}},{"name":"scalar preview context","method":"POST","path":"/actuator/flags/kill","body":{"action":"evaluate","context":true},"expect":{"status":{"pyfly":400,"larafly":400},"error":"bad-request"}}]}
diff --git a/packages/feature-flags/tests/Conformance/management-vectors.json b/packages/feature-flags/tests/Conformance/management-vectors.json
new file mode 100644
index 00000000..7badc839
--- /dev/null
+++ b/packages/feature-flags/tests/Conformance/management-vectors.json
@@ -0,0 +1 @@
+{"version":1,"cases":[{"name":"new put with a failed refresh is pending, not deleted","initialDefinition":null,"action":"put","definition":{"state":"ENABLED","variants":{"on":true,"off":false},"defaultVariant":"on"},"refresh":"fail","expect":{"body":{"key":"checkout","refreshPending":true}}},{"name":"new put with a refused refresh is pending, not deleted","initialDefinition":null,"action":"put","definition":{"state":"ENABLED","variants":{"on":true,"off":false},"defaultVariant":"on"},"refresh":"refuse","expect":{"body":{"key":"checkout","refreshPending":true}}},{"name":"updating a visible flag during a refresh outage returns last good detail","initialDefinition":{"state":"ENABLED","variants":{"on":true,"off":false},"defaultVariant":"off"},"action":"put","definition":{"state":"ENABLED","variants":{"on":true,"off":false},"defaultVariant":"on"},"refresh":"fail","expect":{"detailDefinition":{"state":"ENABLED","variants":{"on":true,"off":false},"defaultVariant":"off"}}},{"name":"deleting during a refresh outage returns the still visible last good detail","initialDefinition":{"state":"ENABLED","variants":{"on":true,"off":false},"defaultVariant":"off"},"action":"delete","refresh":"fail","expect":{"detailDefinition":{"state":"ENABLED","variants":{"on":true,"off":false},"defaultVariant":"off"}}},{"name":"deleting the only definition after successful refresh reports deletion","initialDefinition":{"state":"ENABLED","variants":{"on":true,"off":false},"defaultVariant":"off"},"action":"delete","refresh":"accept","expect":{"body":{"key":"checkout","deleted":true}}}]}
diff --git a/packages/feature-flags/tests/Conformance/observer-vectors.json b/packages/feature-flags/tests/Conformance/observer-vectors.json
new file mode 100644
index 00000000..d2bcbf83
--- /dev/null
+++ b/packages/feature-flags/tests/Conformance/observer-vectors.json
@@ -0,0 +1 @@
+{"version":1,"cases":[{"name":"put source load fails","action":"put","faults":["source_error"],"expect":{"committed":true,"updatedAttempts":1,"visible":"previous"}},{"name":"put refresh is refused","action":"put","faults":["refresh_refused"],"expect":{"committed":true,"updatedAttempts":1,"visible":"previous"}},{"name":"put refresh throws","action":"put","faults":["refresh_throw"],"expect":{"committed":true,"updatedAttempts":1,"visible":"previous"}},{"name":"put change listener throws","action":"put","faults":["change_listener"],"expect":{"committed":true,"updatedAttempts":1,"visible":"current"}},{"name":"put update listener throws","action":"put","faults":["update_listener"],"expect":{"committed":true,"updatedAttempts":1,"visible":"current"}},{"name":"put refresh and update listener fail","action":"put","faults":["source_error","update_listener"],"expect":{"committed":true,"updatedAttempts":1,"visible":"previous"}},{"name":"put warning logger throws","action":"put","faults":["source_error","logger"],"expect":{"committed":true,"updatedAttempts":1,"visible":"previous"}},{"name":"delete source load fails","action":"delete","faults":["source_error"],"expect":{"committed":true,"updatedAttempts":1,"visible":"previous"}},{"name":"delete refresh throws and update listener throws","action":"delete","faults":["refresh_throw","update_listener"],"expect":{"committed":true,"updatedAttempts":1,"visible":"previous"}},{"name":"delete warning logger throws","action":"delete","faults":["source_error","logger"],"expect":{"committed":true,"updatedAttempts":1,"visible":"previous"}}]}
diff --git a/packages/feature-flags/tests/Conformance/override-vectors.json b/packages/feature-flags/tests/Conformance/override-vectors.json
new file mode 100644
index 00000000..952b2cc1
--- /dev/null
+++ b/packages/feature-flags/tests/Conformance/override-vectors.json
@@ -0,0 +1 @@
+{"version":1,"cases":[{"name":"test overrides shadow config only in the local composition","key":"checkout","config":{"checkout":false},"overrides":{"checkout":true},"expect":{"origin":"test-overrides","overrides":["config"],"definition":{"state":"ENABLED","variants":{"on":true,"off":false},"defaultVariant":"on"},"sharedDefinition":{"state":"ENABLED","variants":{"on":true,"off":false},"defaultVariant":"off"},"changedKeys":["checkout"],"eventOrigin":"test-overrides"}},{"name":"test-only flags never enter the shared composition","key":"checkout","config":{},"overrides":{"checkout":true},"expect":{"origin":"test-overrides","overrides":[],"definition":{"state":"ENABLED","variants":{"on":true,"off":false},"defaultVariant":"on"},"sharedDefinition":null,"changedKeys":["checkout"],"eventOrigin":"test-overrides"}}]}
diff --git a/packages/feature-flags/tests/Conformance/store-concurrency-vectors.json b/packages/feature-flags/tests/Conformance/store-concurrency-vectors.json
new file mode 100644
index 00000000..b9f02c73
--- /dev/null
+++ b/packages/feature-flags/tests/Conformance/store-concurrency-vectors.json
@@ -0,0 +1 @@
+{"version":1,"cases":[{"name":"postgresql root expected 0","backend":"postgresql","transaction":"root","expectedVersion":0,"writers":2,"initiallyAbsent":true,"isolation":"READ COMMITTED","snapshotIsolation":false,"expect":{"outcome":"one-conflict","version":1,"auditRows":1,"callerMarkers":0}},{"name":"postgresql root expected None","backend":"postgresql","transaction":"root","expectedVersion":null,"writers":2,"initiallyAbsent":true,"isolation":"READ COMMITTED","snapshotIsolation":false,"expect":{"outcome":"two-writes","version":2,"auditRows":2,"callerMarkers":0}},{"name":"postgresql outer expected 0","backend":"postgresql","transaction":"outer","expectedVersion":0,"writers":2,"initiallyAbsent":true,"isolation":"READ COMMITTED","snapshotIsolation":false,"expect":{"outcome":"one-conflict","version":1,"auditRows":1,"callerMarkers":2}},{"name":"postgresql outer expected None","backend":"postgresql","transaction":"outer","expectedVersion":null,"writers":2,"initiallyAbsent":true,"isolation":"READ COMMITTED","snapshotIsolation":false,"expect":{"outcome":"one-conflict","version":1,"auditRows":1,"callerMarkers":2}},{"name":"postgresql outer-snapshot expected 0","backend":"postgresql","transaction":"outer-snapshot","expectedVersion":0,"writers":2,"initiallyAbsent":true,"isolation":"READ COMMITTED","snapshotIsolation":false,"expect":{"outcome":"one-conflict","version":1,"auditRows":1,"callerMarkers":2}},{"name":"postgresql outer-snapshot expected None","backend":"postgresql","transaction":"outer-snapshot","expectedVersion":null,"writers":2,"initiallyAbsent":true,"isolation":"READ COMMITTED","snapshotIsolation":false,"expect":{"outcome":"one-conflict","version":1,"auditRows":1,"callerMarkers":2}},{"name":"mysql root expected 0","backend":"mysql","transaction":"root","expectedVersion":0,"writers":2,"initiallyAbsent":true,"isolation":"REPEATABLE READ","snapshotIsolation":false,"expect":{"outcome":"one-conflict","version":1,"auditRows":1,"callerMarkers":0}},{"name":"mysql root expected None","backend":"mysql","transaction":"root","expectedVersion":null,"writers":2,"initiallyAbsent":true,"isolation":"REPEATABLE READ","snapshotIsolation":false,"expect":{"outcome":"two-writes","version":2,"auditRows":2,"callerMarkers":0}},{"name":"mysql outer expected 0","backend":"mysql","transaction":"outer","expectedVersion":0,"writers":2,"initiallyAbsent":true,"isolation":"REPEATABLE READ","snapshotIsolation":false,"expect":{"outcome":"one-conflict","version":1,"auditRows":1,"callerMarkers":2}},{"name":"mysql outer expected None","backend":"mysql","transaction":"outer","expectedVersion":null,"writers":2,"initiallyAbsent":true,"isolation":"REPEATABLE READ","snapshotIsolation":false,"expect":{"outcome":"one-conflict","version":1,"auditRows":1,"callerMarkers":2}},{"name":"mysql outer-snapshot expected 0","backend":"mysql","transaction":"outer-snapshot","expectedVersion":0,"writers":2,"initiallyAbsent":true,"isolation":"REPEATABLE READ","snapshotIsolation":false,"expect":{"outcome":"one-conflict","version":1,"auditRows":1,"callerMarkers":2}},{"name":"mysql outer-snapshot expected None","backend":"mysql","transaction":"outer-snapshot","expectedVersion":null,"writers":2,"initiallyAbsent":true,"isolation":"REPEATABLE READ","snapshotIsolation":false,"expect":{"outcome":"one-conflict","version":1,"auditRows":1,"callerMarkers":2}},{"name":"mariadb root expected 0","backend":"mariadb","transaction":"root","expectedVersion":0,"writers":2,"initiallyAbsent":true,"isolation":"REPEATABLE READ","snapshotIsolation":true,"expect":{"outcome":"one-conflict","version":1,"auditRows":1,"callerMarkers":0}},{"name":"mariadb root expected None","backend":"mariadb","transaction":"root","expectedVersion":null,"writers":2,"initiallyAbsent":true,"isolation":"REPEATABLE READ","snapshotIsolation":true,"expect":{"outcome":"two-writes","version":2,"auditRows":2,"callerMarkers":0}},{"name":"mariadb outer expected 0","backend":"mariadb","transaction":"outer","expectedVersion":0,"writers":2,"initiallyAbsent":true,"isolation":"REPEATABLE READ","snapshotIsolation":true,"expect":{"outcome":"one-conflict","version":1,"auditRows":1,"callerMarkers":2}},{"name":"mariadb outer expected None","backend":"mariadb","transaction":"outer","expectedVersion":null,"writers":2,"initiallyAbsent":true,"isolation":"REPEATABLE READ","snapshotIsolation":true,"expect":{"outcome":"one-parent-abort","version":1,"auditRows":1,"callerMarkers":1}},{"name":"mariadb outer-snapshot expected 0","backend":"mariadb","transaction":"outer-snapshot","expectedVersion":0,"writers":2,"initiallyAbsent":true,"isolation":"REPEATABLE READ","snapshotIsolation":true,"expect":{"outcome":"one-parent-abort","version":1,"auditRows":1,"callerMarkers":1}},{"name":"mariadb outer-snapshot expected None","backend":"mariadb","transaction":"outer-snapshot","expectedVersion":null,"writers":2,"initiallyAbsent":true,"isolation":"REPEATABLE READ","snapshotIsolation":true,"expect":{"outcome":"one-parent-abort","version":1,"auditRows":1,"callerMarkers":1}}]}
diff --git a/packages/feature-flags/tests/Conformance/testbed/LICENSE b/packages/feature-flags/tests/Conformance/testbed/LICENSE
new file mode 100644
index 00000000..96b3dc8f
--- /dev/null
+++ b/packages/feature-flags/tests/Conformance/testbed/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright OpenFeature Maintainers
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/packages/feature-flags/tests/Conformance/testbed/SOURCE.md b/packages/feature-flags/tests/Conformance/testbed/SOURCE.md
new file mode 100644
index 00000000..b32ff8c2
--- /dev/null
+++ b/packages/feature-flags/tests/Conformance/testbed/SOURCE.md
@@ -0,0 +1,16 @@
+# Vendored flagd evaluator conformance suite
+
+These files are copied verbatim from the OpenFeature flagd test bed:
+
+- Repository: https://github.com/open-feature/flagd-testbed
+- Tag: `v3.10.2`
+- Commit: `31de75b6466e4eff5f13b8bf3b487db90011d8b0`
+- Paths: `evaluator/flags/testkit-flags.json` and `evaluator/gherkin/*.feature`
+
+The flagd test bed is part of the OpenFeature project, a Cloud Native Computing Foundation project whose
+contributions are licensed under the Apache License 2.0 (the organization's license is reproduced in `LICENSE`).
+The repository itself does not carry a license file.
+
+Firefly runs every scenario except those tagged `@fractional-v1`, the legacy float bucketing that the reference
+evaluator (`openfeature-flagd-core` 1.0.0) no longer implements. Do not edit these files: update them by copying a
+newer tag in full, regenerating `../MANIFEST.sha256`, and doing the same in the other framework.
diff --git a/packages/feature-flags/tests/Conformance/testbed/evaluator/flags/testkit-flags.json b/packages/feature-flags/tests/Conformance/testbed/evaluator/flags/testkit-flags.json
new file mode 100644
index 00000000..cd55c18d
--- /dev/null
+++ b/packages/feature-flags/tests/Conformance/testbed/evaluator/flags/testkit-flags.json
@@ -0,0 +1,712 @@
+{
+ "flags": {
+ "boolean-flag": {
+ "state": "ENABLED",
+ "variants": {
+ "on": true,
+ "off": false
+ },
+ "defaultVariant": "on"
+ },
+ "string-flag": {
+ "state": "ENABLED",
+ "variants": {
+ "greeting": "hi",
+ "parting": "bye"
+ },
+ "defaultVariant": "greeting"
+ },
+ "integer-flag": {
+ "state": "ENABLED",
+ "variants": {
+ "one": 1,
+ "ten": 10
+ },
+ "defaultVariant": "ten"
+ },
+ "float-flag": {
+ "state": "ENABLED",
+ "variants": {
+ "tenth": 0.1,
+ "half": 0.5
+ },
+ "defaultVariant": "half"
+ },
+ "object-flag": {
+ "state": "ENABLED",
+ "variants": {
+ "empty": {},
+ "template": {
+ "showImages": true,
+ "title": "Check out these pics!",
+ "imagesPerPage": 100
+ }
+ },
+ "defaultVariant": "template"
+ },
+ "boolean-zero-flag": {
+ "state": "ENABLED",
+ "variants": {
+ "zero": false,
+ "non-zero": true
+ },
+ "defaultVariant": "zero"
+ },
+ "string-zero-flag": {
+ "state": "ENABLED",
+ "variants": {
+ "zero": "",
+ "non-zero": "str"
+ },
+ "defaultVariant": "zero"
+ },
+ "integer-zero-flag": {
+ "state": "ENABLED",
+ "variants": {
+ "zero": 0,
+ "non-zero": 1
+ },
+ "defaultVariant": "zero"
+ },
+ "float-zero-flag": {
+ "state": "ENABLED",
+ "variants": {
+ "zero": 0.0,
+ "non-zero": 1.0
+ },
+ "defaultVariant": "zero"
+ },
+ "object-zero-flag": {
+ "state": "ENABLED",
+ "variants": {
+ "zero": {},
+ "non-zero": {
+ "showImages": true,
+ "title": "Check out these pics!",
+ "imagesPerPage": 100
+ }
+ },
+ "defaultVariant": "zero"
+ },
+ "boolean-targeted-zero-flag": {
+ "state": "ENABLED",
+ "variants": {
+ "zero": false,
+ "non-zero": true
+ },
+ "targeting": {
+ "if": [{"$ref": "is_ballmer"}, "zero"]
+ },
+ "defaultVariant": "zero"
+ },
+ "string-targeted-zero-flag": {
+ "state": "ENABLED",
+ "variants": {
+ "zero": "",
+ "non-zero": "str"
+ },
+ "targeting": {
+ "if": [{"$ref": "is_ballmer"}, "zero"]
+ },
+ "defaultVariant": "zero"
+ },
+ "integer-targeted-zero-flag": {
+ "state": "ENABLED",
+ "variants": {
+ "zero": 0,
+ "non-zero": 1
+ },
+ "targeting": {
+ "if": [{"$ref": "is_ballmer"}, "zero"]
+ },
+ "defaultVariant": "zero"
+ },
+ "float-targeted-zero-flag": {
+ "state": "ENABLED",
+ "variants": {
+ "zero": 0.0,
+ "non-zero": 1.0
+ },
+ "targeting": {
+ "if": [{"$ref": "is_ballmer"}, "zero"]
+ },
+ "defaultVariant": "zero"
+ },
+ "wrong-flag": {
+ "state": "ENABLED",
+ "variants": {
+ "one": "uno",
+ "two": "dos"
+ },
+ "defaultVariant": "one"
+ },
+ "null-default-flag": {
+ "state": "ENABLED",
+ "variants": {
+ "on": true,
+ "off": false
+ },
+ "defaultVariant": null
+ },
+ "undefined-default-flag": {
+ "state": "ENABLED",
+ "variants": {
+ "small": 10,
+ "big": 1000
+ }
+ },
+ "no-default-flag-null-targeting-variant": {
+ "state": "ENABLED",
+ "variants": {
+ "normal": "CFO",
+ "special": "CEO"
+ },
+ "targeting": {
+ "if": [
+ {"==": ["jobs@orange.com", {"var": ["email"]}]},
+ "special",
+ null
+ ]
+ }
+ },
+ "no-default-flag-undefined-targeting-variant": {
+ "state": "ENABLED",
+ "variants": {
+ "normal": "CFO",
+ "special": "CEO"
+ },
+ "targeting": {
+ "if": [
+ {"==": ["jobs@orange.com", {"var": ["email"]}]},
+ "special"
+ ]
+ }
+ },
+ "metadata-flag": {
+ "state": "ENABLED",
+ "variants": {
+ "on": true,
+ "off": false
+ },
+ "defaultVariant": "on",
+ "metadata": {
+ "string": "1.0.2",
+ "integer": 2,
+ "boolean": true,
+ "float": 0.1
+ }
+ },
+ "some-email-targeted-flag": {
+ "state": "ENABLED",
+ "variants": {
+ "hi": "hi",
+ "bye": "bye",
+ "none": "none"
+ },
+ "defaultVariant": "none",
+ "targeting": {
+ "if": [{"$ref": "is_ballmer"}, "hi", "bye"]
+ }
+ },
+ "some-other-email-targeted-flag": {
+ "state": "ENABLED",
+ "variants": {
+ "yes": "yes",
+ "no": "no",
+ "none": "none"
+ },
+ "defaultVariant": "none",
+ "targeting": {
+ "if": [{"$ref": "is_ballmer"}, "yes", "no"]
+ }
+ },
+ "fractional-flag": {
+ "state": "ENABLED",
+ "variants": {
+ "clubs": "clubs",
+ "diamonds": "diamonds",
+ "hearts": "hearts",
+ "spades": "spades",
+ "wild": "wild"
+ },
+ "defaultVariant": "wild",
+ "targeting": {
+ "fractional": [
+ {"cat": [{"var": "$flagd.flagKey"}, {"var": "user.name"}]},
+ ["clubs", 25],
+ ["diamonds", 25],
+ ["hearts", 25],
+ ["spades", 25]
+ ]
+ }
+ },
+ "fractional-flag-shorthand": {
+ "state": "ENABLED",
+ "variants": {
+ "heads": "heads",
+ "tails": "tails",
+ "draw": "draw"
+ },
+ "defaultVariant": "draw",
+ "targeting": {
+ "fractional": [
+ ["heads"],
+ ["tails", 1]
+ ]
+ }
+ },
+ "fractional-flag-A-shared-seed": {
+ "state": "ENABLED",
+ "variants": {
+ "clubs": "clubs",
+ "diamonds": "diamonds",
+ "hearts": "hearts",
+ "spades": "spades",
+ "wild": "wild"
+ },
+ "defaultVariant": "wild",
+ "targeting": {
+ "fractional": [
+ {"cat": ["shared-seed", {"var": "user.name"}]},
+ ["clubs", 25],
+ ["diamonds", 25],
+ ["hearts", 25],
+ ["spades", 25]
+ ]
+ }
+ },
+ "fractional-flag-B-shared-seed": {
+ "state": "ENABLED",
+ "variants": {
+ "clubs": "ace-of-clubs",
+ "diamonds": "ace-of-diamonds",
+ "hearts": "ace-of-hearts",
+ "spades": "ace-of-spades",
+ "wild": "wild"
+ },
+ "defaultVariant": "wild",
+ "targeting": {
+ "fractional": [
+ {"cat": ["shared-seed", {"var": "user.name"}]},
+ ["clubs", 25],
+ ["diamonds", 25],
+ ["hearts", 25],
+ ["spades", 25]
+ ]
+ }
+ },
+ "fractional-hash-edge-flag": {
+ "state": "ENABLED",
+ "variants": { "lower": "lower", "upper": "upper" },
+ "defaultVariant": "lower",
+ "targeting": {
+ "fractional": [
+ { "var": "targetingKey" },
+ [ "lower", 50 ],
+ [ "upper", 50 ]
+ ]
+ }
+ },
+ "starts-ends-flag": {
+ "state": "ENABLED",
+ "variants": {
+ "prefix": "prefix",
+ "postfix": "postfix",
+ "none": "none"
+ },
+ "defaultVariant": "none",
+ "targeting": {
+ "if": [
+ {"starts_with": [{"var": "id"}, "abc"]},
+ "prefix",
+ {"if": [
+ {"ends_with": [{"var": "id"}, "xyz"]},
+ "postfix",
+ "none"
+ ]}
+ ]
+ }
+ },
+ "equal-greater-lesser-version-flag": {
+ "state": "ENABLED",
+ "variants": {
+ "equal": "equal",
+ "greater": "greater",
+ "lesser": "lesser",
+ "none": "none"
+ },
+ "defaultVariant": "none",
+ "targeting": {
+ "if": [
+ {"sem_ver": [{"var": "version"}, "=", "2.0.0"]},
+ "equal",
+ {"if": [
+ {"sem_ver": [{"var": "version"}, ">", "2.0.0"]},
+ "greater",
+ {"if": [
+ {"sem_ver": [{"var": "version"}, "<", "2.0.0"]},
+ "lesser",
+ "none"
+ ]}
+ ]}
+ ]
+ }
+ },
+ "major-minor-version-flag": {
+ "state": "ENABLED",
+ "variants": {
+ "minor": "minor",
+ "major": "major",
+ "none": "none"
+ },
+ "defaultVariant": "none",
+ "targeting": {
+ "if": [
+ {"sem_ver": [{"var": "version"}, "~", "3.0.0"]},
+ "minor",
+ {"if": [
+ {"sem_ver": [{"var": "version"}, "^", "3.0.0"]},
+ "major",
+ "none"
+ ]}
+ ]
+ }
+ },
+ "targeting-key-flag": {
+ "state": "ENABLED",
+ "variants": {
+ "miss": "miss",
+ "hit": "hit"
+ },
+ "defaultVariant": "miss",
+ "targeting": {
+ "if": [
+ {"==": [{"var": "targetingKey"}, "5c3d8535-f81a-4478-a6d3-afaa4d51199e"]},
+ "hit",
+ null
+ ]
+ }
+ },
+ "fractional-nested-if-flag": {
+ "state": "ENABLED",
+ "variants": { "premium": "premium", "standard": "standard", "fallback": "fallback" },
+ "defaultVariant": "fallback",
+ "targeting": {
+ "fractional": [
+ {"var": "targetingKey"},
+ [{"if": [{"==": [{"var": "tier"}, "premium"]}, "premium", "standard"]}, 50],
+ ["standard", 50]
+ ]
+ }
+ },
+ "fractional-nested-var-flag": {
+ "state": "ENABLED",
+ "variants": { "red": "red", "green": "green", "blue": "blue", "fallback": "fallback" },
+ "defaultVariant": "fallback",
+ "targeting": {
+ "fractional": [
+ {"var": "targetingKey"},
+ [{"var": "color"}, 50],
+ ["blue", 50]
+ ]
+ }
+ },
+ "fractional-nested-weight-flag": {
+ "state": "ENABLED",
+ "variants": { "red": "red", "blue": "blue", "fallback": "fallback" },
+ "defaultVariant": "fallback",
+ "targeting": {
+ "fractional": [
+ {"var": "targetingKey"},
+ ["red", {"if": [{"==": [{"var": "tier"}, "premium"]}, 100, 0]}],
+ ["blue", 10]
+ ]
+ }
+ },
+ "fractional-as-condition-flag": {
+ "state": "ENABLED",
+ "variants": { "big": "hundreds", "small": "ones", "fallback": "zero" },
+ "defaultVariant": "fallback",
+ "targeting": {
+ "if": [
+ {"fractional": [[false, 0], [true, 100]]},
+ "big",
+ "small"
+ ]
+ }
+ },
+ "fractional-as-condition-false-flag": {
+ "state": "ENABLED",
+ "variants": { "big": "hundreds", "small": "ones", "fallback": "zero" },
+ "defaultVariant": "fallback",
+ "targeting": {
+ "if": [
+ {"fractional": [[false, 100], [true, 0]]},
+ "big",
+ "small"
+ ]
+ }
+ },
+ "fractional-single-entry-flag": {
+ "state": "ENABLED",
+ "variants": {
+ "single": "single",
+ "fallback": "fallback"
+ },
+ "defaultVariant": "fallback",
+ "targeting": {
+ "fractional": [
+ [ "single", 1 ]
+ ]
+ }
+ },
+ "semver-invalid-version-flag": {
+ "state": "ENABLED",
+ "variants": { "true": "true", "false": "false", "fallback": "fallback" },
+ "defaultVariant": "fallback",
+ "targeting": {
+ "sem_ver": [{"var": "version"}, "=", "1.0.0"]
+ }
+ },
+ "semver-invalid-operator-flag": {
+ "state": "ENABLED",
+ "variants": { "true": "true", "false": "false", "fallback": "fallback" },
+ "defaultVariant": "fallback",
+ "targeting": {
+ "sem_ver": [{"var": "version"}, "===", "1.0.0"]
+ }
+ },
+ "fractional-null-bucket-key-flag": {
+ "state": "ENABLED",
+ "variants": { "one": "one", "two": "two", "fallback": "fallback" },
+ "defaultVariant": "fallback",
+ "targeting": {
+ "fractional": [
+ {"var": "missing_key"},
+ ["one", 50],
+ ["two", 50]
+ ]
+ }
+ },
+ "semver-v-prefix-flag": {
+ "state": "ENABLED",
+ "variants": { "match": "match", "no-match": "no-match" },
+ "defaultVariant": "no-match",
+ "targeting": {
+ "if": [{"sem_ver": [{"var": "version"}, "=", "v1.0.0"]}, "match", "no-match"]
+ }
+ },
+ "semver-partial-version-flag": {
+ "state": "ENABLED",
+ "variants": { "match": "match", "no-match": "no-match" },
+ "defaultVariant": "no-match",
+ "targeting": {
+ "if": [{"sem_ver": [{"var": "version"}, "^", "1"]}, "match", "no-match"]
+ }
+ },
+ "semver-build-metadata-flag": {
+ "state": "ENABLED",
+ "variants": { "match": "match", "no-match": "no-match" },
+ "defaultVariant": "no-match",
+ "targeting": {
+ "if": [{"sem_ver": [{"var": "version"}, "=", "1.0.0+build"]}, "match", "no-match"]
+ }
+ },
+ "semver-numeric-context-flag": {
+ "state": "ENABLED",
+ "variants": { "match": "match", "no-match": "no-match" },
+ "defaultVariant": "no-match",
+ "targeting": {
+ "if": [{"sem_ver": [{"var": "version"}, ">", "1.1"]}, "match", "no-match"]
+ }
+ },
+ "ref-whitespace-compact-flag": {
+ "state": "ENABLED",
+ "variants": {
+ "hi": "hi",
+ "bye": "bye",
+ "fallback": "fallback"
+ },
+ "defaultVariant": "fallback",
+ "targeting": {
+ "if": [
+ {
+ "$ref":"is_ballmer"
+ },
+ "hi",
+ "bye"
+ ]
+ }
+ },
+ "ref-whitespace-space-after-colon-flag": {
+ "state": "ENABLED",
+ "variants": {
+ "hi": "hi",
+ "bye": "bye",
+ "fallback": "fallback"
+ },
+ "defaultVariant": "fallback",
+ "targeting": {
+ "if": [
+ {
+ "$ref": "is_ballmer"
+ },
+ "hi",
+ "bye"
+ ]
+ }
+ },
+ "ref-whitespace-space-around-colon-flag": {
+ "state": "ENABLED",
+ "variants": {
+ "hi": "hi",
+ "bye": "bye",
+ "fallback": "fallback"
+ },
+ "defaultVariant": "fallback",
+ "targeting": {
+ "if": [
+ {
+ "$ref" : "is_ballmer"
+ },
+ "hi",
+ "bye"
+ ]
+ }
+ },
+ "ref-to-nonexistent-evaluator-flag": {
+ "state": "ENABLED",
+ "variants": {
+ "hit": "hit",
+ "miss": "miss",
+ "fallback": "fallback"
+ },
+ "defaultVariant": "fallback",
+ "targeting": {
+ "if": [
+ {
+ "$ref": "nonexistent_evaluator"
+ },
+ "hit",
+ "miss"
+ ]
+ }
+ },
+ "starts-with-non-string-flag": {
+ "state": "ENABLED",
+ "variants": { "true": "true", "false": "false", "fallback": "fallback" },
+ "defaultVariant": "fallback",
+ "targeting": {
+ "starts_with": [{"var": "num"}, "abc"]
+ }
+ },
+ "ends-with-non-string-flag": {
+ "state": "ENABLED",
+ "variants": { "true": "true", "false": "false", "fallback": "fallback" },
+ "defaultVariant": "fallback",
+ "targeting": {
+ "ends_with": [{"var": "num"}, "xyz"]
+ }
+ },
+ "starts-with-wrong-args-flag": {
+ "state": "ENABLED",
+ "variants": { "true": "true", "false": "false", "fallback": "fallback" },
+ "defaultVariant": "fallback",
+ "targeting": {
+ "starts_with": ["abc"]
+ }
+ },
+ "ends-with-wrong-args-flag": {
+ "state": "ENABLED",
+ "variants": { "true": "true", "false": "false", "fallback": "fallback" },
+ "defaultVariant": "fallback",
+ "targeting": {
+ "ends_with": ["xyz"]
+ }
+ },
+ "fractional-zero-weights-flag": {
+ "state": "ENABLED",
+ "variants": { "one": "one", "two": "two", "fallback": "fallback" },
+ "defaultVariant": "fallback",
+ "targeting": {
+ "fractional": [
+ {"var": "targetingKey"},
+ ["one", 0],
+ ["two", 0]
+ ]
+ }
+ },
+ "fractional-negative-weight-flag": {
+ "state": "ENABLED",
+ "variants": { "one": "one", "two": "two", "fallback": "fallback" },
+ "defaultVariant": "fallback",
+ "targeting": {
+ "fractional": [
+ {"var": "targetingKey"},
+ ["one", -50],
+ ["two", 100]
+ ]
+ }
+ },
+ "semver-wrong-args-flag": {
+ "state": "ENABLED",
+ "variants": { "true": "true", "false": "false", "fallback": "fallback" },
+ "defaultVariant": "fallback",
+ "targeting": {
+ "sem_ver": [{"var": "version"}, "="]
+ }
+ },
+ "disabled-boolean-flag": {
+ "state": "DISABLED",
+ "variants": {
+ "on": true,
+ "off": false
+ },
+ "defaultVariant": "on"
+ },
+ "disabled-string-flag": {
+ "state": "DISABLED",
+ "variants": {
+ "greeting": "hi",
+ "parting": "bye"
+ },
+ "defaultVariant": "greeting"
+ },
+ "disabled-integer-flag": {
+ "state": "DISABLED",
+ "variants": {
+ "one": 1,
+ "ten": 10
+ },
+ "defaultVariant": "ten"
+ },
+ "disabled-float-flag": {
+ "state": "DISABLED",
+ "variants": {
+ "tenth": 0.1,
+ "half": 0.5
+ },
+ "defaultVariant": "half"
+ },
+ "disabled-object-flag": {
+ "state": "DISABLED",
+ "variants": {
+ "empty": {},
+ "template": {
+ "showImages": true,
+ "title": "Check out these pics!",
+ "imagesPerPage": 100
+ }
+ },
+ "defaultVariant": "template"
+ }
+ },
+ "$evaluators": {
+ "is_ballmer": {
+ "==": [
+ "ballmer@macrosoft.com",
+ {"var": ["email"]}
+ ]
+ }
+ }
+}
diff --git a/packages/feature-flags/tests/Conformance/testbed/evaluator/gherkin/disabled.feature b/packages/feature-flags/tests/Conformance/testbed/evaluator/gherkin/disabled.feature
new file mode 100644
index 00000000..5edff08b
--- /dev/null
+++ b/packages/feature-flags/tests/Conformance/testbed/evaluator/gherkin/disabled.feature
@@ -0,0 +1,22 @@
+@disabled
+Feature: Evaluator disabled flag evaluation
+
+ # Validates that disabled flags resolve with reason=DISABLED. The evaluator
+ # substitutes the caller-provided default value and omits the variant.
+ # Flags are configured in evaluator/flags/testkit-flags.json.
+ # Relates to: https://github.com/open-feature/flagd/issues/1965
+
+ Scenario Outline: Resolve disabled flag returns reason DISABLED with the default value
+ Given an evaluator
+ And a -flag with key "" and a fallback value ""
+ When the flag was evaluated with details
+ Then the resolved details value should be ""
+ And the reason should be "DISABLED"
+
+ Examples:
+ | key | type | default |
+ | disabled-boolean-flag | Boolean | false |
+ | disabled-string-flag | String | bye |
+ | disabled-integer-flag | Integer | 1 |
+ | disabled-float-flag | Float | 0.1 |
+ | disabled-object-flag | Object | {} |
diff --git a/packages/feature-flags/tests/Conformance/testbed/evaluator/gherkin/errors.feature b/packages/feature-flags/tests/Conformance/testbed/evaluator/gherkin/errors.feature
new file mode 100644
index 00000000..2ff2f54a
--- /dev/null
+++ b/packages/feature-flags/tests/Conformance/testbed/evaluator/gherkin/errors.feature
@@ -0,0 +1,18 @@
+Feature: Evaluator error handling
+
+ # Validates that the evaluator returns the correct error codes for
+ # well-known error conditions: FLAG_NOT_FOUND and TYPE_MISMATCH.
+ # Flags are configured in evaluator/flags/testkit-flags.json.
+
+ Background:
+ Given an evaluator
+
+ Scenario: Flag not found
+ Given a String-flag with key "missing-flag" and a fallback value "uh-oh"
+ When the flag was evaluated with details
+ Then the error-code should be "FLAG_NOT_FOUND"
+
+ Scenario: Type mismatch
+ Given a Integer-flag with key "wrong-flag" and a fallback value "13"
+ When the flag was evaluated with details
+ Then the error-code should be "TYPE_MISMATCH"
diff --git a/packages/feature-flags/tests/Conformance/testbed/evaluator/gherkin/evaluation.feature b/packages/feature-flags/tests/Conformance/testbed/evaluator/gherkin/evaluation.feature
new file mode 100644
index 00000000..701291a9
--- /dev/null
+++ b/packages/feature-flags/tests/Conformance/testbed/evaluator/gherkin/evaluation.feature
@@ -0,0 +1,27 @@
+Feature: Evaluator basic flag evaluation
+
+ # Validates basic static flag resolution for all supported types.
+ # Flags are configured in evaluator/flags/testkit-flags.json.
+
+ Scenario Outline: Resolve basic values
+ Given an evaluator
+ And a -flag with key "" and a fallback value ""
+ When the flag was evaluated with details
+ Then the resolved details value should be ""
+
+ Examples: Boolean evaluations
+ | key | type | default | resolved_value |
+ | boolean-flag | Boolean | false | true |
+
+ Examples: String evaluations
+ | key | type | default | resolved_value |
+ | string-flag | String | bye | hi |
+
+ Examples: Number evaluations
+ | key | type | default | resolved_value |
+ | integer-flag | Integer | 1 | 10 |
+ | float-flag | Float | 0.1 | 0.5 |
+
+ Examples: Object evaluations
+ | key | type | default | resolved_value |
+ | object-flag | Object | {} | {\"showImages\": true,\"title\": \"Check out these pics!\",\"imagesPerPage\": 100} |
diff --git a/packages/feature-flags/tests/Conformance/testbed/evaluator/gherkin/evaluator-refs.feature b/packages/feature-flags/tests/Conformance/testbed/evaluator/gherkin/evaluator-refs.feature
new file mode 100644
index 00000000..1e2ffd4f
--- /dev/null
+++ b/packages/feature-flags/tests/Conformance/testbed/evaluator/gherkin/evaluator-refs.feature
@@ -0,0 +1,40 @@
+Feature: Evaluator evaluator refs
+
+ # Tests that $ref shared targeting rules work correctly.
+
+ Scenario Outline: Evaluator reuse via $ref
+ Given an evaluator
+ And a String-flag with key "" and a fallback value "fallback"
+ And a context containing a key "email", with type "String" and with value "ballmer@macrosoft.com"
+ When the flag was evaluated with details
+ Then the resolved details value should be ""
+
+ Examples:
+ | key | value |
+ | some-email-targeted-flag | hi |
+ | some-other-email-targeted-flag | yes |
+
+ @evaluator-refs @evaluator-refs-whitespace
+ Scenario Outline: Evaluator $ref resolves regardless of whitespace around the colon
+ Given an evaluator
+ And a String-flag with key "" and a fallback value "fallback"
+ And a context containing a key "email", with type "String" and with value ""
+ When the flag was evaluated with details
+ Then the resolved details value should be ""
+ And the reason should be "TARGETING_MATCH"
+ Examples:
+ | key | email | value |
+ | ref-whitespace-compact-flag | ballmer@macrosoft.com | hi |
+ | ref-whitespace-space-after-colon-flag | ballmer@macrosoft.com | hi |
+ | ref-whitespace-space-around-colon-flag | ballmer@macrosoft.com | hi |
+ | ref-whitespace-compact-flag | user@example.com | bye |
+ | ref-whitespace-space-after-colon-flag | user@example.com | bye |
+ | ref-whitespace-space-around-colon-flag | user@example.com | bye |
+
+ @evaluator-refs @non-existent-evaluator-ref
+ Scenario: Ref to nonexistent evaluator yields parse error
+ Given an evaluator
+ And a String-flag with key "ref-to-nonexistent-evaluator-flag" and a fallback value "fallback"
+ When the flag was evaluated with details
+ Then the resolved details value should be "fallback"
+ And the error-code should be "PARSE_ERROR"
diff --git a/packages/feature-flags/tests/Conformance/testbed/evaluator/gherkin/fractional.feature b/packages/feature-flags/tests/Conformance/testbed/evaluator/gherkin/fractional.feature
new file mode 100644
index 00000000..a58fc373
--- /dev/null
+++ b/packages/feature-flags/tests/Conformance/testbed/evaluator/gherkin/fractional.feature
@@ -0,0 +1,218 @@
+@fractional
+Feature: Evaluator fractional operator
+
+ # Tests the fractional bucketing operator for consistent user assignment.
+ # @fractional-v1: legacy float-based bucketing (abs(hash) / i32::MAX * 100)
+ # @fractional-v2: high-precision integer bucketing ((hash * totalWeight) >> 32)
+
+ Background:
+ Given an evaluator
+
+ Scenario Outline: Fractional operator
+ Given a String-flag with key "fractional-flag" and a fallback value "fallback"
+ And a context containing a nested property with outer key "user" and inner key "name", with value ""
+ When the flag was evaluated with details
+ Then the resolved details value should be ""
+
+ @fractional-v1
+ Examples: v1
+ | name | value |
+ | jack | spades |
+ | queen | clubs |
+ | ten | diamonds |
+ | nine | hearts |
+ | 3 | diamonds |
+
+ @fractional-v2
+ Examples: v2
+ | name | value |
+ | jack | hearts |
+ | queen | spades |
+ | ten | clubs |
+ | nine | diamonds |
+ | 3 | clubs |
+
+ Scenario Outline: Fractional operator shorthand
+ Given a String-flag with key "fractional-flag-shorthand" and a fallback value "fallback"
+ And a context containing a targeting key with value ""
+ When the flag was evaluated with details
+ Then the resolved details value should be ""
+
+ @fractional-v1
+ Examples: v1
+ | targeting_key | value |
+ | jon@company.com | heads |
+ | jane@company.com | tails |
+
+ @fractional-v2
+ Examples: v2
+ | targeting_key | value |
+ | jon@company.com | heads |
+ | jane@company.com | tails |
+
+ Scenario Outline: Fractional operator with shared seed
+ Given a String-flag with key "fractional-flag-A-shared-seed" and a fallback value "fallback"
+ And a context containing a nested property with outer key "user" and inner key "name", with value ""
+ When the flag was evaluated with details
+ Then the resolved details value should be ""
+
+ @fractional-v1
+ Examples: v1
+ | name | value |
+ | jack | hearts |
+ | queen | spades |
+ | ten | hearts |
+ | nine | diamonds |
+
+ @fractional-v2
+ Examples: v2
+ | name | value |
+ | seven | hearts |
+ | eight | diamonds |
+ | nine | clubs |
+ | two | spades |
+
+ @fractional-v2
+ Scenario: Fractional operator with single entry always resolves to the only variant
+ Given a String-flag with key "fractional-single-entry-flag" and a fallback value "fallback"
+ And a context containing a targeting key with value "some-targeting-key"
+ When the flag was evaluated with details
+ Then the resolved details value should be "single"
+
+ Scenario Outline: Second fractional operator with shared seed
+ Given a String-flag with key "fractional-flag-B-shared-seed" and a fallback value "fallback"
+ And a context containing a nested property with outer key "user" and inner key "name", with value ""
+ When the flag was evaluated with details
+ Then the resolved details value should be ""
+
+ @fractional-v1
+ Examples: v1
+ | name | value |
+ | jack | ace-of-hearts |
+ | queen | ace-of-spades |
+ | ten | ace-of-hearts |
+ | nine | ace-of-diamonds |
+
+ @fractional-v2
+ Examples: v2
+ | name | value |
+ | seven | ace-of-hearts |
+ | eight | ace-of-diamonds |
+ | nine | ace-of-clubs |
+ | two | ace-of-spades |
+
+ # Hash edge-case vectors — keys chosen by brute-force search so their
+ # MurmurHash3-x86-32 (seed=0) falls at the six critical boundary values.
+ @fractional-v2
+ Scenario Outline: Fractional operator hash edge cases
+ Given a String-flag with key "fractional-hash-edge-flag" and a fallback value "fallback"
+ And a context containing a targeting key with value ""
+ When the flag was evaluated with details
+ Then the resolved details value should be ""
+
+ Examples:
+ | key | value |
+ | ejOoVL | lower |
+ | bY9fO- | lower |
+ | SI7p- | lower |
+ | 6LvT0 | upper |
+ | ceQdGm | upper |
+
+ # Nested JSON Logic expressions as bucket variant names / weights.
+ # Requires evaluator implementations to support the @fractional-nested feature.
+ # Use -t "not @fractional-nested" to exclude during transition.
+
+ @fractional-nested
+ Scenario Outline: Fractional operator with nested if expression as variant name
+ # bucket0=[if(tier=="premium","premium","standard"),50], bucket1=["standard",50]
+ # jon@company.com bv(100)=36 → bucket0; user1 bv(100)=76 → bucket1
+ Given an evaluator
+ And a String-flag with key "fractional-nested-if-flag" and a fallback value "fallback"
+ And a context containing a targeting key with value ""
+ And a context containing a key "tier", with type "String" and with value ""
+ When the flag was evaluated with details
+ Then the resolved details value should be ""
+ Examples:
+ | targetingKey | tier | value |
+ | jon@company.com | premium | premium |
+ | jon@company.com | basic | standard |
+ | user1 | premium | standard |
+ | user1 | basic | standard |
+
+ @fractional-nested
+ Scenario Outline: Fractional operator with nested var expression as variant name
+ # bucket0=[var("color"),50], bucket1=["blue",50]
+ # jon@company.com bv(100)=36 → bucket0 (resolves var "color"); user1 bv(100)=76 → bucket1 ("blue")
+ Given an evaluator
+ And a String-flag with key "fractional-nested-var-flag" and a fallback value "fallback"
+ And a context containing a targeting key with value ""
+ And a context containing a key "color", with type "String" and with value ""
+ When the flag was evaluated with details
+ Then the resolved details value should be ""
+ Examples:
+ | targetingKey | color | value |
+ | jon@company.com | red | red |
+ | jon@company.com | green | green |
+ | user1 | red | blue |
+ | jon@company.com | yellow | fallback |
+ | jon@company.com | | fallback |
+
+ @fractional-nested
+ Scenario Outline: Fractional operator with nested if expression as weight
+ # bucket0=["red",if(tier=="premium",100,0)], bucket1=["blue",10]
+ Given an evaluator
+ And a String-flag with key "fractional-nested-weight-flag" and a fallback value "fallback"
+ And a context containing a targeting key with value ""
+ And a context containing a key "tier", with type "String" and with value ""
+ When the flag was evaluated with details
+ Then the resolved details value should be ""
+ Examples:
+ | targetingKey | tier | value |
+ | jon@company.com | premium | red |
+ | jon@company.com | basic | blue |
+ | user1 | premium | red |
+ | user1 | basic | blue |
+
+ @fractional-nested
+ Scenario: Fractional as condition
+ Given an evaluator
+ And a String-flag with key "fractional-as-condition-flag" and a fallback value "zero"
+ And a context containing a targeting key with value "some-targeting-key"
+ When the flag was evaluated with details
+ Then the resolved details value should be "hundreds"
+
+ @fractional-nested
+ Scenario: Fractional as condition evaluates false path
+ Given an evaluator
+ And a String-flag with key "fractional-as-condition-false-flag" and a fallback value "zero"
+ And a context containing a targeting key with value "some-targeting-key"
+ When the flag was evaluated with details
+ Then the resolved details value should be "ones"
+
+ @operator-errors
+ Scenario: fractional operator with missing bucket key falls back to default variant
+ Given an evaluator
+ And a String-flag with key "fractional-null-bucket-key-flag" and a fallback value "wrong"
+ When the flag was evaluated with details
+ Then the resolved details value should be "fallback"
+ And the reason should be "DEFAULT"
+
+ # Follow-up error scenarios from https://github.com/open-feature/flagd/issues/1874
+
+ @operator-errors
+ Scenario: fractional with all-zero bucket weights falls back to default variant
+ Given an evaluator
+ And a String-flag with key "fractional-zero-weights-flag" and a fallback value "wrong"
+ And a context containing a targeting key with value "any-user"
+ When the flag was evaluated with details
+ Then the resolved details value should be "fallback"
+ And the reason should be "DEFAULT"
+
+ @operator-errors
+ Scenario: fractional negative bucket weight is clamped to zero
+ # ["one", -50] is treated as ["one", 0]; "two" gets 100% of the weight
+ Given an evaluator
+ And a String-flag with key "fractional-negative-weight-flag" and a fallback value "wrong"
+ And a context containing a targeting key with value "any-user"
+ When the flag was evaluated with details
+ Then the resolved details value should be "two"
diff --git a/packages/feature-flags/tests/Conformance/testbed/evaluator/gherkin/metadata.feature b/packages/feature-flags/tests/Conformance/testbed/evaluator/gherkin/metadata.feature
new file mode 100644
index 00000000..8f24e8db
--- /dev/null
+++ b/packages/feature-flags/tests/Conformance/testbed/evaluator/gherkin/metadata.feature
@@ -0,0 +1,30 @@
+@metadata
+Feature: Evaluator flag metadata
+
+ # This test suite validates metadata handling in the Evaluator interface.
+ # It is associated with the flags configured in evaluator/flags/testkit-flags.json.
+
+ Background:
+ Given an evaluator
+
+ Scenario: Returns flag metadata
+ Given a Boolean-flag with key "metadata-flag" and a fallback value "false"
+ When the flag was evaluated with details
+ Then the resolved metadata should contain
+ | key | metadata_type | value |
+ | string | String | 1.0.2 |
+ | integer | Integer | 2 |
+ | float | Float | 0.1 |
+ | boolean | Boolean | true |
+
+ Scenario Outline: Returns no metadata for flags without metadata
+ Given a -flag with key "" and a fallback value ""
+ When the flag was evaluated with details
+ Then the resolved metadata is empty
+
+ Examples:
+ | key | flag_type | default_value |
+ | boolean-flag | Boolean | true |
+ | integer-flag | Integer | 23 |
+ | float-flag | Float | 2.3 |
+ | string-flag | String | value |
diff --git a/packages/feature-flags/tests/Conformance/testbed/evaluator/gherkin/no-default-variant.feature b/packages/feature-flags/tests/Conformance/testbed/evaluator/gherkin/no-default-variant.feature
new file mode 100644
index 00000000..3844b8fe
--- /dev/null
+++ b/packages/feature-flags/tests/Conformance/testbed/evaluator/gherkin/no-default-variant.feature
@@ -0,0 +1,23 @@
+@no-default-variant
+Feature: Evaluator no-default-variant handling
+
+ # Validates correct behavior when a flag's defaultVariant is null or undefined.
+ # In such cases the evaluator falls back to the code-default supplied by the caller.
+ # Flags are configured in evaluator/flags/testkit-flags.json.
+
+ Scenario Outline: Resolve flag with no default variant
+ Given an evaluator
+ And a -flag with key "" and a fallback value ""
+ And a context containing a key "email", with type "String" and with value ""
+ When the flag was evaluated with details
+ Then the resolved details value should be ""
+ And the reason should be ""
+
+ Examples:
+ | key | type | email | code_default | resolved_value | reason |
+ | null-default-flag | Boolean | | true | true | DEFAULT |
+ | null-default-flag | Boolean | | false | false | DEFAULT |
+ | undefined-default-flag | Integer | | 100 | 100 | DEFAULT |
+ | no-default-flag-null-targeting-variant | String | wozniak@orange.com | Inventor | Inventor | DEFAULT |
+ | no-default-flag-null-targeting-variant | String | jobs@orange.com | CEO | CEO | TARGETING_MATCH |
+ | no-default-flag-undefined-targeting-variant | String | wozniak@orange.com | Retired | Retired | DEFAULT |
diff --git a/packages/feature-flags/tests/Conformance/testbed/evaluator/gherkin/semver.feature b/packages/feature-flags/tests/Conformance/testbed/evaluator/gherkin/semver.feature
new file mode 100644
index 00000000..1b136f2d
--- /dev/null
+++ b/packages/feature-flags/tests/Conformance/testbed/evaluator/gherkin/semver.feature
@@ -0,0 +1,117 @@
+@semver
+Feature: Evaluator semantic version operator
+
+ # Tests the sem_ver custom operator for semantic version comparisons.
+
+ Background:
+ Given an evaluator
+
+ Scenario Outline: Numeric comparison
+ Given a String-flag with key "equal-greater-lesser-version-flag" and a fallback value "fallback"
+ And a context containing a key "version", with type "String" and with value ""
+ When the flag was evaluated with details
+ Then the resolved details value should be ""
+
+ Examples:
+ | version | value |
+ | 2.0.0 | equal |
+ | 2.1.0 | greater |
+ | 1.9.0 | lesser |
+ | 2.0.0-alpha | lesser |
+ | 2.0.0.0 | none |
+
+ Scenario Outline: Semantic comparison (minor/major range)
+ Given a String-flag with key "major-minor-version-flag" and a fallback value "fallback"
+ And a context containing a key "version", with type "String" and with value ""
+ When the flag was evaluated with details
+ Then the resolved details value should be ""
+
+ Examples:
+ | version | value |
+ | 3.0.1 | minor |
+ | 3.1.0 | major |
+ | 4.0.0 | none |
+
+ @operator-errors
+ Scenario Outline: sem_ver returns null for invalid input and falls back to default variant
+ Given an evaluator
+ And a String-flag with key "" and a fallback value "wrong"
+ And a context containing a key "version", with type "String" and with value ""
+ When the flag was evaluated with details
+ Then the resolved details value should be "fallback"
+ And the reason should be "DEFAULT"
+ Examples:
+ | key | context_value |
+ | semver-invalid-version-flag | not-a-version |
+ | semver-invalid-operator-flag | 1.0.0 |
+
+ @semver-edge-cases @semver-v-prefix
+ Scenario Outline: v-prefix handling
+ Given an evaluator
+ And a String-flag with key "semver-v-prefix-flag" and a fallback value "fallback"
+ And a context containing a key "version", with type "String" and with value ""
+ When the flag was evaluated with details
+ Then the resolved details value should be ""
+ Examples:
+ | version | value |
+ | 1.0.0 | match |
+ | v1.0.0 | match |
+ | V1.0.0 | match |
+ | 2.0.0 | no-match |
+
+ @semver-edge-cases @semver-partial-version
+ Scenario Outline: partial version string handling
+ Given an evaluator
+ And a String-flag with key "semver-partial-version-flag" and a fallback value "fallback"
+ And a context containing a key "version", with type "" and with value ""
+ When the flag was evaluated with details
+ Then the resolved details value should be ""
+ Examples:
+ | version | type | value |
+ | 1.5.0 | String | match |
+ | 1.0.0 | String | match |
+ | 1.0 | String | match |
+ | 1 | String | match |
+ | 1 | Integer | match |
+ | 1 | Float | match |
+ | 1.2 | Float | match |
+ | 2.0.0 | String | no-match |
+
+ @semver-edge-cases @semver-numeric-context
+ Scenario Outline: numeric context value coercion
+ Given an evaluator
+ And a String-flag with key "semver-numeric-context-flag" and a fallback value "fallback"
+ And a context containing a key "version", with type "" and with value ""
+ When the flag was evaluated with details
+ Then the resolved details value should be ""
+ Examples:
+ | version | type | value |
+ | 1.2 | Float | match |
+ | 1.1 | Float | no-match |
+ | 2 | Integer | match |
+ | 1 | Integer | no-match |
+ | 1.2 | String | match |
+
+ @semver-edge-cases @semver-build-metadata
+ Scenario Outline: build metadata ignored in comparison
+ Given an evaluator
+ And a String-flag with key "semver-build-metadata-flag" and a fallback value "fallback"
+ And a context containing a key "version", with type "String" and with value ""
+ When the flag was evaluated with details
+ Then the resolved details value should be ""
+ Examples:
+ | version | value |
+ | 1.0.0 | match |
+ | 1.0.0+other | match |
+ | 2.0.0 | no-match |
+
+ # Follow-up error scenarios from https://github.com/open-feature/flagd/issues/1874
+
+ @operator-errors
+ Scenario: sem_ver returns null for wrong argument count
+ Given an evaluator
+ And a String-flag with key "semver-wrong-args-flag" and a fallback value "wrong"
+ And a context containing a key "version", with type "String" and with value "1.0.0"
+ When the flag was evaluated with details
+ Then the resolved details value should be "fallback"
+ And the reason should be "DEFAULT"
diff --git a/packages/feature-flags/tests/Conformance/testbed/evaluator/gherkin/string.feature b/packages/feature-flags/tests/Conformance/testbed/evaluator/gherkin/string.feature
new file mode 100644
index 00000000..7e77176d
--- /dev/null
+++ b/packages/feature-flags/tests/Conformance/testbed/evaluator/gherkin/string.feature
@@ -0,0 +1,47 @@
+@string
+Feature: Evaluator string comparison operator
+
+ # Tests the starts_with and ends_with custom operators.
+
+ Scenario Outline: Substring operators
+ Given an evaluator
+ And a String-flag with key "starts-ends-flag" and a fallback value "fallback"
+ And a context containing a key "id", with type "String" and with value ""
+ When the flag was evaluated with details
+ Then the resolved details value should be ""
+
+ Examples:
+ | id | value |
+ | abcdef | prefix |
+ | uvwxyz | postfix |
+ | abcxyz | prefix |
+ | lmnopq | none |
+
+ # Follow-up error scenarios from https://github.com/open-feature/flagd/issues/1874
+ # starts_with and ends_with must return null (not false) on error so that the
+ # default variant is selected, rather than looking up a non-existent "false" variant.
+
+ @operator-errors
+ Scenario Outline: starts_with and ends_with return null for non-string input
+ Given an evaluator
+ And a String-flag with key "" and a fallback value "wrong"
+ And a context containing a key "num", with type "Integer" and with value "123"
+ When the flag was evaluated with details
+ Then the resolved details value should be "fallback"
+ And the reason should be "DEFAULT"
+ Examples:
+ | key |
+ | starts-with-non-string-flag |
+ | ends-with-non-string-flag |
+
+ @operator-errors
+ Scenario Outline: starts_with and ends_with return null for wrong argument count
+ Given an evaluator
+ And a String-flag with key "" and a fallback value "wrong"
+ When the flag was evaluated with details
+ Then the resolved details value should be "fallback"
+ And the reason should be "DEFAULT"
+ Examples:
+ | key |
+ | starts-with-wrong-args-flag |
+ | ends-with-wrong-args-flag |
diff --git a/packages/feature-flags/tests/Conformance/testbed/evaluator/gherkin/targeting.feature b/packages/feature-flags/tests/Conformance/testbed/evaluator/gherkin/targeting.feature
new file mode 100644
index 00000000..3722a8a8
--- /dev/null
+++ b/packages/feature-flags/tests/Conformance/testbed/evaluator/gherkin/targeting.feature
@@ -0,0 +1,17 @@
+@targeting
+Feature: Evaluator targeting
+
+ # Tests context-based targeting rules including targeting key evaluation.
+
+ Scenario Outline: Targeting by targeting key
+ Given an evaluator
+ And a String-flag with key "targeting-key-flag" and a fallback value "fallback"
+ And a context containing a targeting key with value ""
+ When the flag was evaluated with details
+ Then the resolved details value should be ""
+ And the reason should be ""
+
+ Examples:
+ | targeting_key | value | reason |
+ | 5c3d8535-f81a-4478-a6d3-afaa4d51199e | hit | TARGETING_MATCH |
+ | f20bd32d-703b-48b6-bc8e-79d53c85134a | miss | DEFAULT |
diff --git a/packages/feature-flags/tests/Conformance/testbed/evaluator/gherkin/zero-values.feature b/packages/feature-flags/tests/Conformance/testbed/evaluator/gherkin/zero-values.feature
new file mode 100644
index 00000000..1ccfabe4
--- /dev/null
+++ b/packages/feature-flags/tests/Conformance/testbed/evaluator/gherkin/zero-values.feature
@@ -0,0 +1,73 @@
+Feature: Evaluator zero value resolution
+
+ # Validates that zero/falsy/empty values are correctly resolved,
+ # both in static flags and when selected via targeting rules.
+ # Flags are configured in evaluator/flags/testkit-flags.json.
+
+ Background:
+ Given an evaluator
+
+ Scenario Outline: Resolve zero values statically
+ Given a -flag with key "" and a fallback value "