@contractspec/lib.feature-flags 1.57.0 → 1.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/browser/contracts/index.js +636 -0
  2. package/dist/browser/docs/feature-flags.docblock.js +71 -0
  3. package/dist/browser/docs/index.js +71 -0
  4. package/dist/browser/entities/index.js +306 -0
  5. package/dist/browser/evaluation/index.js +223 -0
  6. package/dist/browser/events.js +296 -0
  7. package/dist/browser/feature-flags.capability.js +28 -0
  8. package/dist/browser/feature-flags.feature.js +55 -0
  9. package/dist/browser/index.js +1583 -0
  10. package/dist/contracts/index.d.ts +944 -950
  11. package/dist/contracts/index.d.ts.map +1 -1
  12. package/dist/contracts/index.js +635 -906
  13. package/dist/docs/feature-flags.docblock.d.ts +2 -1
  14. package/dist/docs/feature-flags.docblock.d.ts.map +1 -0
  15. package/dist/docs/feature-flags.docblock.js +18 -22
  16. package/dist/docs/index.d.ts +2 -1
  17. package/dist/docs/index.d.ts.map +1 -0
  18. package/dist/docs/index.js +72 -1
  19. package/dist/entities/index.d.ts +159 -164
  20. package/dist/entities/index.d.ts.map +1 -1
  21. package/dist/entities/index.js +297 -315
  22. package/dist/evaluation/index.d.ts +119 -122
  23. package/dist/evaluation/index.d.ts.map +1 -1
  24. package/dist/evaluation/index.js +215 -212
  25. package/dist/events.d.ts +480 -486
  26. package/dist/events.d.ts.map +1 -1
  27. package/dist/events.js +272 -511
  28. package/dist/feature-flags.capability.d.ts +2 -7
  29. package/dist/feature-flags.capability.d.ts.map +1 -1
  30. package/dist/feature-flags.capability.js +29 -25
  31. package/dist/feature-flags.feature.d.ts +1 -6
  32. package/dist/feature-flags.feature.d.ts.map +1 -1
  33. package/dist/feature-flags.feature.js +54 -146
  34. package/dist/index.d.ts +7 -6
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +1584 -8
  37. package/dist/node/contracts/index.js +636 -0
  38. package/dist/node/docs/feature-flags.docblock.js +71 -0
  39. package/dist/node/docs/index.js +71 -0
  40. package/dist/node/entities/index.js +306 -0
  41. package/dist/node/evaluation/index.js +223 -0
  42. package/dist/node/events.js +296 -0
  43. package/dist/node/feature-flags.capability.js +28 -0
  44. package/dist/node/feature-flags.feature.js +55 -0
  45. package/dist/node/index.js +1583 -0
  46. package/package.json +117 -30
  47. package/dist/contracts/index.js.map +0 -1
  48. package/dist/docs/feature-flags.docblock.js.map +0 -1
  49. package/dist/entities/index.js.map +0 -1
  50. package/dist/evaluation/index.js.map +0 -1
  51. package/dist/events.js.map +0 -1
  52. package/dist/feature-flags.capability.js.map +0 -1
  53. package/dist/feature-flags.feature.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,8 +1,1584 @@
1
- import { ExperimentCreatedEvent, ExperimentStartedEvent, ExperimentStoppedEvent, FeatureFlagEvents, FlagCreatedEvent, FlagDeletedEvent, FlagEvaluatedEvent, FlagToggledEvent, FlagUpdatedEvent, RuleCreatedEvent, RuleDeletedEvent, VariantAssignedEvent } from "./events.js";
2
- import { FeatureFlagsFeature } from "./feature-flags.feature.js";
3
- import { ExperimentAssignmentEntity, ExperimentEntity, ExperimentStatusEnum, FeatureFlagEntity, FlagEvaluationEntity, FlagStatusEnum, FlagTargetingRuleEntity, RuleOperatorEnum, featureFlagEntities, featureFlagsSchemaContribution } from "./entities/index.js";
4
- import { CreateExperimentContract, CreateFlagContract, CreateRuleContract, DeleteFlagContract, DeleteRuleContract, EvaluateFlagContract, EvaluationResultModel, ExperimentModel, FeatureFlagModel, GetExperimentContract, GetFlagContract, ListFlagsContract, StartExperimentContract, StopExperimentContract, TargetingRuleModel, ToggleFlagContract, UpdateFlagContract } from "./contracts/index.js";
5
- import { FlagEvaluator, InMemoryFlagRepository, evaluateRuleCondition, getSubjectId, hashToBucket } from "./evaluation/index.js";
6
- import "./docs/index.js";
7
-
8
- export { CreateExperimentContract, CreateFlagContract, CreateRuleContract, DeleteFlagContract, DeleteRuleContract, EvaluateFlagContract, EvaluationResultModel, ExperimentAssignmentEntity, ExperimentCreatedEvent, ExperimentEntity, ExperimentModel, ExperimentStartedEvent, ExperimentStatusEnum, ExperimentStoppedEvent, FeatureFlagEntity, FeatureFlagEvents, FeatureFlagModel, FeatureFlagsFeature, FlagCreatedEvent, FlagDeletedEvent, FlagEvaluatedEvent, FlagEvaluationEntity, FlagEvaluator, FlagStatusEnum, FlagTargetingRuleEntity, FlagToggledEvent, FlagUpdatedEvent, GetExperimentContract, GetFlagContract, InMemoryFlagRepository, ListFlagsContract, RuleCreatedEvent, RuleDeletedEvent, RuleOperatorEnum, StartExperimentContract, StopExperimentContract, TargetingRuleModel, ToggleFlagContract, UpdateFlagContract, VariantAssignedEvent, evaluateRuleCondition, featureFlagEntities, featureFlagsSchemaContribution, getSubjectId, hashToBucket };
1
+ // @bun
2
+ // src/contracts/index.ts
3
+ import { ScalarTypeEnum, defineSchemaModel } from "@contractspec/lib.schema";
4
+ import { defineCommand, defineQuery } from "@contractspec/lib.contracts";
5
+ var OWNERS = ["platform.feature-flags"];
6
+ var FeatureFlagModel = defineSchemaModel({
7
+ name: "FeatureFlag",
8
+ description: "Represents a feature flag",
9
+ fields: {
10
+ id: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
11
+ key: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
12
+ name: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
13
+ description: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
14
+ status: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
15
+ defaultValue: { type: ScalarTypeEnum.Boolean(), isOptional: false },
16
+ variants: { type: ScalarTypeEnum.JSON(), isOptional: true },
17
+ orgId: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
18
+ tags: { type: ScalarTypeEnum.JSON(), isOptional: true },
19
+ createdAt: { type: ScalarTypeEnum.DateTime(), isOptional: false },
20
+ updatedAt: { type: ScalarTypeEnum.DateTime(), isOptional: false }
21
+ }
22
+ });
23
+ var TargetingRuleModel = defineSchemaModel({
24
+ name: "TargetingRule",
25
+ description: "Represents a targeting rule",
26
+ fields: {
27
+ id: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
28
+ flagId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
29
+ name: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
30
+ priority: { type: ScalarTypeEnum.Int_unsecure(), isOptional: false },
31
+ enabled: { type: ScalarTypeEnum.Boolean(), isOptional: false },
32
+ attribute: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
33
+ operator: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
34
+ value: { type: ScalarTypeEnum.JSON(), isOptional: false },
35
+ rolloutPercentage: {
36
+ type: ScalarTypeEnum.Int_unsecure(),
37
+ isOptional: true
38
+ },
39
+ serveValue: { type: ScalarTypeEnum.Boolean(), isOptional: true },
40
+ serveVariant: { type: ScalarTypeEnum.String_unsecure(), isOptional: true }
41
+ }
42
+ });
43
+ var ExperimentModel = defineSchemaModel({
44
+ name: "Experiment",
45
+ description: "Represents an experiment",
46
+ fields: {
47
+ id: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
48
+ key: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
49
+ name: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
50
+ description: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
51
+ hypothesis: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
52
+ flagId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
53
+ status: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
54
+ variants: { type: ScalarTypeEnum.JSON(), isOptional: false },
55
+ metrics: { type: ScalarTypeEnum.JSON(), isOptional: true },
56
+ audiencePercentage: {
57
+ type: ScalarTypeEnum.Int_unsecure(),
58
+ isOptional: false
59
+ },
60
+ startedAt: { type: ScalarTypeEnum.DateTime(), isOptional: true },
61
+ endedAt: { type: ScalarTypeEnum.DateTime(), isOptional: true },
62
+ winningVariant: {
63
+ type: ScalarTypeEnum.String_unsecure(),
64
+ isOptional: true
65
+ },
66
+ results: { type: ScalarTypeEnum.JSON(), isOptional: true },
67
+ createdAt: { type: ScalarTypeEnum.DateTime(), isOptional: false }
68
+ }
69
+ });
70
+ var EvaluationResultModel = defineSchemaModel({
71
+ name: "EvaluationResult",
72
+ description: "Result of flag evaluation",
73
+ fields: {
74
+ enabled: { type: ScalarTypeEnum.Boolean(), isOptional: false },
75
+ variant: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
76
+ reason: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
77
+ ruleId: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
78
+ experimentId: { type: ScalarTypeEnum.String_unsecure(), isOptional: true }
79
+ }
80
+ });
81
+ var CreateFlagInput = defineSchemaModel({
82
+ name: "CreateFlagInput",
83
+ description: "Input for creating a feature flag",
84
+ fields: {
85
+ key: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
86
+ name: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
87
+ description: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
88
+ defaultValue: { type: ScalarTypeEnum.Boolean(), isOptional: true },
89
+ variants: { type: ScalarTypeEnum.JSON(), isOptional: true },
90
+ orgId: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
91
+ tags: { type: ScalarTypeEnum.JSON(), isOptional: true }
92
+ }
93
+ });
94
+ var UpdateFlagInput = defineSchemaModel({
95
+ name: "UpdateFlagInput",
96
+ description: "Input for updating a feature flag",
97
+ fields: {
98
+ flagId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
99
+ name: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
100
+ description: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
101
+ defaultValue: { type: ScalarTypeEnum.Boolean(), isOptional: true },
102
+ variants: { type: ScalarTypeEnum.JSON(), isOptional: true },
103
+ tags: { type: ScalarTypeEnum.JSON(), isOptional: true }
104
+ }
105
+ });
106
+ var DeleteFlagInput = defineSchemaModel({
107
+ name: "DeleteFlagInput",
108
+ description: "Input for deleting a feature flag",
109
+ fields: {
110
+ flagId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false }
111
+ }
112
+ });
113
+ var ToggleFlagInput = defineSchemaModel({
114
+ name: "ToggleFlagInput",
115
+ description: "Input for toggling a feature flag",
116
+ fields: {
117
+ flagId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
118
+ status: { type: ScalarTypeEnum.String_unsecure(), isOptional: false }
119
+ }
120
+ });
121
+ var GetFlagInput = defineSchemaModel({
122
+ name: "GetFlagInput",
123
+ description: "Input for getting a feature flag",
124
+ fields: {
125
+ key: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
126
+ orgId: { type: ScalarTypeEnum.String_unsecure(), isOptional: true }
127
+ }
128
+ });
129
+ var ListFlagsInput = defineSchemaModel({
130
+ name: "ListFlagsInput",
131
+ description: "Input for listing feature flags",
132
+ fields: {
133
+ orgId: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
134
+ status: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
135
+ tags: { type: ScalarTypeEnum.JSON(), isOptional: true },
136
+ limit: { type: ScalarTypeEnum.Int_unsecure(), isOptional: true },
137
+ offset: { type: ScalarTypeEnum.Int_unsecure(), isOptional: true }
138
+ }
139
+ });
140
+ var ListFlagsOutput = defineSchemaModel({
141
+ name: "ListFlagsOutput",
142
+ description: "Output for listing feature flags",
143
+ fields: {
144
+ flags: { type: FeatureFlagModel, isArray: true, isOptional: false },
145
+ total: { type: ScalarTypeEnum.Int_unsecure(), isOptional: false }
146
+ }
147
+ });
148
+ var EvaluateFlagInput = defineSchemaModel({
149
+ name: "EvaluateFlagInput",
150
+ description: "Input for evaluating a feature flag",
151
+ fields: {
152
+ key: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
153
+ context: { type: ScalarTypeEnum.JSON(), isOptional: false }
154
+ }
155
+ });
156
+ var CreateRuleInput = defineSchemaModel({
157
+ name: "CreateRuleInput",
158
+ description: "Input for creating a targeting rule",
159
+ fields: {
160
+ flagId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
161
+ name: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
162
+ priority: { type: ScalarTypeEnum.Int_unsecure(), isOptional: true },
163
+ attribute: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
164
+ operator: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
165
+ value: { type: ScalarTypeEnum.JSON(), isOptional: false },
166
+ rolloutPercentage: {
167
+ type: ScalarTypeEnum.Int_unsecure(),
168
+ isOptional: true
169
+ },
170
+ serveValue: { type: ScalarTypeEnum.Boolean(), isOptional: true },
171
+ serveVariant: { type: ScalarTypeEnum.String_unsecure(), isOptional: true }
172
+ }
173
+ });
174
+ var DeleteRuleInput = defineSchemaModel({
175
+ name: "DeleteRuleInput",
176
+ description: "Input for deleting a targeting rule",
177
+ fields: {
178
+ ruleId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false }
179
+ }
180
+ });
181
+ var CreateExperimentInput = defineSchemaModel({
182
+ name: "CreateExperimentInput",
183
+ description: "Input for creating an experiment",
184
+ fields: {
185
+ key: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
186
+ name: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
187
+ description: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
188
+ hypothesis: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
189
+ flagId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
190
+ variants: { type: ScalarTypeEnum.JSON(), isOptional: false },
191
+ metrics: { type: ScalarTypeEnum.JSON(), isOptional: true },
192
+ audiencePercentage: {
193
+ type: ScalarTypeEnum.Int_unsecure(),
194
+ isOptional: true
195
+ },
196
+ scheduledStartAt: { type: ScalarTypeEnum.DateTime(), isOptional: true },
197
+ scheduledEndAt: { type: ScalarTypeEnum.DateTime(), isOptional: true },
198
+ orgId: { type: ScalarTypeEnum.String_unsecure(), isOptional: true }
199
+ }
200
+ });
201
+ var StartExperimentInput = defineSchemaModel({
202
+ name: "StartExperimentInput",
203
+ description: "Input for starting an experiment",
204
+ fields: {
205
+ experimentId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false }
206
+ }
207
+ });
208
+ var StopExperimentInput = defineSchemaModel({
209
+ name: "StopExperimentInput",
210
+ description: "Input for stopping an experiment",
211
+ fields: {
212
+ experimentId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
213
+ reason: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
214
+ winningVariant: {
215
+ type: ScalarTypeEnum.String_unsecure(),
216
+ isOptional: true
217
+ }
218
+ }
219
+ });
220
+ var GetExperimentInput = defineSchemaModel({
221
+ name: "GetExperimentInput",
222
+ description: "Input for getting an experiment",
223
+ fields: {
224
+ experimentId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false }
225
+ }
226
+ });
227
+ var SuccessOutput = defineSchemaModel({
228
+ name: "SuccessOutput",
229
+ description: "Generic success output",
230
+ fields: {
231
+ success: { type: ScalarTypeEnum.Boolean(), isOptional: false }
232
+ }
233
+ });
234
+ var CreateFlagContract = defineCommand({
235
+ meta: {
236
+ key: "flag.create",
237
+ version: "1.0.0",
238
+ stability: "stable",
239
+ owners: [...OWNERS],
240
+ tags: ["feature-flags", "create"],
241
+ description: "Create a new feature flag.",
242
+ goal: "Define a new feature flag for toggling features.",
243
+ context: "Called when setting up a new feature flag."
244
+ },
245
+ io: {
246
+ input: CreateFlagInput,
247
+ output: FeatureFlagModel,
248
+ errors: {
249
+ KEY_ALREADY_EXISTS: {
250
+ description: "Flag key already exists",
251
+ http: 409,
252
+ gqlCode: "FLAG_KEY_EXISTS",
253
+ when: "A flag with this key already exists"
254
+ }
255
+ }
256
+ },
257
+ policy: {
258
+ auth: "admin"
259
+ }
260
+ });
261
+ var UpdateFlagContract = defineCommand({
262
+ meta: {
263
+ key: "flag.update",
264
+ version: "1.0.0",
265
+ stability: "stable",
266
+ owners: [...OWNERS],
267
+ tags: ["feature-flags", "update"],
268
+ description: "Update an existing feature flag.",
269
+ goal: "Modify flag configuration.",
270
+ context: "Called when adjusting flag settings."
271
+ },
272
+ io: {
273
+ input: UpdateFlagInput,
274
+ output: FeatureFlagModel,
275
+ errors: {
276
+ FLAG_NOT_FOUND: {
277
+ description: "Flag does not exist",
278
+ http: 404,
279
+ gqlCode: "FLAG_NOT_FOUND",
280
+ when: "Flag ID is invalid"
281
+ }
282
+ }
283
+ },
284
+ policy: {
285
+ auth: "admin"
286
+ }
287
+ });
288
+ var DeleteFlagContract = defineCommand({
289
+ meta: {
290
+ key: "flag.delete",
291
+ version: "1.0.0",
292
+ stability: "stable",
293
+ owners: [...OWNERS],
294
+ tags: ["feature-flags", "delete"],
295
+ description: "Delete a feature flag.",
296
+ goal: "Remove a feature flag and all its rules.",
297
+ context: "Called when a flag is no longer needed."
298
+ },
299
+ io: {
300
+ input: DeleteFlagInput,
301
+ output: SuccessOutput,
302
+ errors: {
303
+ FLAG_NOT_FOUND: {
304
+ description: "Flag does not exist",
305
+ http: 404,
306
+ gqlCode: "FLAG_NOT_FOUND",
307
+ when: "Flag ID is invalid"
308
+ },
309
+ FLAG_HAS_ACTIVE_EXPERIMENT: {
310
+ description: "Flag has an active experiment",
311
+ http: 409,
312
+ gqlCode: "FLAG_HAS_ACTIVE_EXPERIMENT",
313
+ when: "Cannot delete flag with running experiment"
314
+ }
315
+ }
316
+ },
317
+ policy: {
318
+ auth: "admin"
319
+ }
320
+ });
321
+ var ToggleFlagContract = defineCommand({
322
+ meta: {
323
+ key: "flag.toggle",
324
+ version: "1.0.0",
325
+ stability: "stable",
326
+ owners: [...OWNERS],
327
+ tags: ["feature-flags", "toggle"],
328
+ description: "Toggle a feature flag status.",
329
+ goal: "Quickly enable or disable a feature.",
330
+ context: "Called when turning a feature on or off."
331
+ },
332
+ io: {
333
+ input: ToggleFlagInput,
334
+ output: FeatureFlagModel,
335
+ errors: {
336
+ FLAG_NOT_FOUND: {
337
+ description: "Flag does not exist",
338
+ http: 404,
339
+ gqlCode: "FLAG_NOT_FOUND",
340
+ when: "Flag ID is invalid"
341
+ },
342
+ INVALID_STATUS: {
343
+ description: "Invalid status value",
344
+ http: 400,
345
+ gqlCode: "INVALID_STATUS",
346
+ when: "Status must be OFF, ON, or GRADUAL"
347
+ }
348
+ }
349
+ },
350
+ policy: {
351
+ auth: "admin"
352
+ }
353
+ });
354
+ var GetFlagContract = defineQuery({
355
+ meta: {
356
+ key: "flag.get",
357
+ version: "1.0.0",
358
+ stability: "stable",
359
+ owners: [...OWNERS],
360
+ tags: ["feature-flags", "get"],
361
+ description: "Get a feature flag by key.",
362
+ goal: "Retrieve flag configuration.",
363
+ context: "Called to inspect flag details."
364
+ },
365
+ io: {
366
+ input: GetFlagInput,
367
+ output: FeatureFlagModel,
368
+ errors: {
369
+ FLAG_NOT_FOUND: {
370
+ description: "Flag does not exist",
371
+ http: 404,
372
+ gqlCode: "FLAG_NOT_FOUND",
373
+ when: "Flag key is invalid"
374
+ }
375
+ }
376
+ },
377
+ policy: {
378
+ auth: "user"
379
+ }
380
+ });
381
+ var ListFlagsContract = defineQuery({
382
+ meta: {
383
+ key: "flag.list",
384
+ version: "1.0.0",
385
+ stability: "stable",
386
+ owners: [...OWNERS],
387
+ tags: ["feature-flags", "list"],
388
+ description: "List all feature flags.",
389
+ goal: "View all configured flags.",
390
+ context: "Admin dashboard."
391
+ },
392
+ io: {
393
+ input: ListFlagsInput,
394
+ output: ListFlagsOutput
395
+ },
396
+ policy: {
397
+ auth: "admin"
398
+ }
399
+ });
400
+ var EvaluateFlagContract = defineQuery({
401
+ meta: {
402
+ key: "flag.evaluate",
403
+ version: "1.0.0",
404
+ stability: "stable",
405
+ owners: [...OWNERS],
406
+ tags: ["feature-flags", "evaluate"],
407
+ description: "Evaluate a feature flag for a given context.",
408
+ goal: "Determine if a feature should be enabled.",
409
+ context: "Called at runtime to check feature availability."
410
+ },
411
+ io: {
412
+ input: EvaluateFlagInput,
413
+ output: EvaluationResultModel,
414
+ errors: {
415
+ FLAG_NOT_FOUND: {
416
+ description: "Flag does not exist",
417
+ http: 404,
418
+ gqlCode: "FLAG_NOT_FOUND",
419
+ when: "Flag key is invalid"
420
+ }
421
+ }
422
+ },
423
+ policy: {
424
+ auth: "anonymous"
425
+ }
426
+ });
427
+ var CreateRuleContract = defineCommand({
428
+ meta: {
429
+ key: "flag.rule.create",
430
+ version: "1.0.0",
431
+ stability: "stable",
432
+ owners: [...OWNERS],
433
+ tags: ["feature-flags", "rule", "create"],
434
+ description: "Create a targeting rule for a flag.",
435
+ goal: "Add conditional targeting to a flag.",
436
+ context: "Called when setting up targeting."
437
+ },
438
+ io: {
439
+ input: CreateRuleInput,
440
+ output: TargetingRuleModel,
441
+ errors: {
442
+ FLAG_NOT_FOUND: {
443
+ description: "Flag does not exist",
444
+ http: 404,
445
+ gqlCode: "FLAG_NOT_FOUND",
446
+ when: "Flag ID is invalid"
447
+ },
448
+ INVALID_OPERATOR: {
449
+ description: "Invalid operator",
450
+ http: 400,
451
+ gqlCode: "INVALID_OPERATOR",
452
+ when: "Operator is not supported"
453
+ }
454
+ }
455
+ },
456
+ policy: {
457
+ auth: "admin"
458
+ }
459
+ });
460
+ var DeleteRuleContract = defineCommand({
461
+ meta: {
462
+ key: "flag.rule.delete",
463
+ version: "1.0.0",
464
+ stability: "stable",
465
+ owners: [...OWNERS],
466
+ tags: ["feature-flags", "rule", "delete"],
467
+ description: "Delete a targeting rule.",
468
+ goal: "Remove a targeting rule from a flag.",
469
+ context: "Called when removing targeting conditions."
470
+ },
471
+ io: {
472
+ input: DeleteRuleInput,
473
+ output: SuccessOutput,
474
+ errors: {
475
+ RULE_NOT_FOUND: {
476
+ description: "Rule does not exist",
477
+ http: 404,
478
+ gqlCode: "RULE_NOT_FOUND",
479
+ when: "Rule ID is invalid"
480
+ }
481
+ }
482
+ },
483
+ policy: {
484
+ auth: "admin"
485
+ }
486
+ });
487
+ var CreateExperimentContract = defineCommand({
488
+ meta: {
489
+ key: "experiment.create",
490
+ version: "1.0.0",
491
+ stability: "stable",
492
+ owners: [...OWNERS],
493
+ tags: ["feature-flags", "experiment", "create"],
494
+ description: "Create an A/B test experiment.",
495
+ goal: "Set up an experiment with variants.",
496
+ context: "Called when setting up A/B testing."
497
+ },
498
+ io: {
499
+ input: CreateExperimentInput,
500
+ output: ExperimentModel,
501
+ errors: {
502
+ FLAG_NOT_FOUND: {
503
+ description: "Flag does not exist",
504
+ http: 404,
505
+ gqlCode: "FLAG_NOT_FOUND",
506
+ when: "Flag ID is invalid"
507
+ },
508
+ EXPERIMENT_KEY_EXISTS: {
509
+ description: "Experiment key already exists",
510
+ http: 409,
511
+ gqlCode: "EXPERIMENT_KEY_EXISTS",
512
+ when: "An experiment with this key already exists"
513
+ },
514
+ INVALID_VARIANTS: {
515
+ description: "Invalid variant configuration",
516
+ http: 400,
517
+ gqlCode: "INVALID_VARIANTS",
518
+ when: "Variant percentages must sum to 100"
519
+ }
520
+ }
521
+ },
522
+ policy: {
523
+ auth: "admin"
524
+ }
525
+ });
526
+ var StartExperimentContract = defineCommand({
527
+ meta: {
528
+ key: "experiment.start",
529
+ version: "1.0.0",
530
+ stability: "stable",
531
+ owners: [...OWNERS],
532
+ tags: ["feature-flags", "experiment", "start"],
533
+ description: "Start an experiment.",
534
+ goal: "Begin collecting data for an experiment.",
535
+ context: "Called when ready to run an A/B test."
536
+ },
537
+ io: {
538
+ input: StartExperimentInput,
539
+ output: ExperimentModel,
540
+ errors: {
541
+ EXPERIMENT_NOT_FOUND: {
542
+ description: "Experiment does not exist",
543
+ http: 404,
544
+ gqlCode: "EXPERIMENT_NOT_FOUND",
545
+ when: "Experiment ID is invalid"
546
+ },
547
+ EXPERIMENT_ALREADY_RUNNING: {
548
+ description: "Experiment is already running",
549
+ http: 409,
550
+ gqlCode: "EXPERIMENT_ALREADY_RUNNING",
551
+ when: "Cannot start an experiment that is already running"
552
+ }
553
+ }
554
+ },
555
+ policy: {
556
+ auth: "admin"
557
+ }
558
+ });
559
+ var StopExperimentContract = defineCommand({
560
+ meta: {
561
+ key: "experiment.stop",
562
+ version: "1.0.0",
563
+ stability: "stable",
564
+ owners: [...OWNERS],
565
+ tags: ["feature-flags", "experiment", "stop"],
566
+ description: "Stop an experiment.",
567
+ goal: "End an experiment and optionally declare a winner.",
568
+ context: "Called when concluding an A/B test."
569
+ },
570
+ io: {
571
+ input: StopExperimentInput,
572
+ output: ExperimentModel,
573
+ errors: {
574
+ EXPERIMENT_NOT_FOUND: {
575
+ description: "Experiment does not exist",
576
+ http: 404,
577
+ gqlCode: "EXPERIMENT_NOT_FOUND",
578
+ when: "Experiment ID is invalid"
579
+ },
580
+ EXPERIMENT_NOT_RUNNING: {
581
+ description: "Experiment is not running",
582
+ http: 409,
583
+ gqlCode: "EXPERIMENT_NOT_RUNNING",
584
+ when: "Cannot stop an experiment that is not running"
585
+ }
586
+ }
587
+ },
588
+ policy: {
589
+ auth: "admin"
590
+ }
591
+ });
592
+ var GetExperimentContract = defineQuery({
593
+ meta: {
594
+ key: "experiment.get",
595
+ version: "1.0.0",
596
+ stability: "stable",
597
+ owners: [...OWNERS],
598
+ tags: ["feature-flags", "experiment", "get"],
599
+ description: "Get experiment details.",
600
+ goal: "View experiment configuration and results.",
601
+ context: "Called to inspect experiment status."
602
+ },
603
+ io: {
604
+ input: GetExperimentInput,
605
+ output: ExperimentModel,
606
+ errors: {
607
+ EXPERIMENT_NOT_FOUND: {
608
+ description: "Experiment does not exist",
609
+ http: 404,
610
+ gqlCode: "EXPERIMENT_NOT_FOUND",
611
+ when: "Experiment ID is invalid"
612
+ }
613
+ }
614
+ },
615
+ policy: {
616
+ auth: "user"
617
+ }
618
+ });
619
+
620
+ // src/docs/feature-flags.docblock.ts
621
+ import { registerDocBlocks } from "@contractspec/lib.contracts/docs";
622
+ var featureFlagsDocBlocks = [
623
+ {
624
+ id: "docs.feature-flags.overview",
625
+ title: "Feature Flags & Experiments",
626
+ summary: "Reusable, spec-first feature flag and experiment module with targeting, gradual rollout, multivariate variants, and evaluation logging.",
627
+ kind: "reference",
628
+ visibility: "public",
629
+ route: "/docs/feature-flags/overview",
630
+ tags: ["feature-flags", "experiments", "progressive-delivery"],
631
+ body: `## What this module provides
632
+
633
+ - **Entities**: FeatureFlag, FlagTargetingRule, Experiment, ExperimentAssignment, FlagEvaluation.
634
+ - **Contracts**: create/update/delete/toggle/list/get flags; create/delete rules; evaluate flags; create/start/stop/get experiments.
635
+ - **Events**: flag.created/updated/deleted/toggled, rule.created/deleted, experiment.created/started/stopped, flag.evaluated, experiment.variant_assigned.
636
+ - **Evaluation Engine**: Deterministic evaluator with gradual rollout, rule priority, audience filters, and experiment bucketing.
637
+
638
+ ## How to use
639
+
640
+ 1) Compose schema
641
+ - Add \`featureFlagsSchemaContribution\` to your module composition.
642
+
643
+ 2) Register contracts/events
644
+ - Import exports from \`@contractspec/lib.feature-flags\` into your spec registry.
645
+
646
+ 3) Evaluate at runtime
647
+ - Instantiate \`FlagEvaluator\` with a repository implementation and optional logger.
648
+ - Evaluate with context attributes (userId, orgId, plan, segment, sessionId, attributes).
649
+
650
+ 4) Wire observability
651
+ - Emit audit trail on config changes; emit \`flag.evaluated\` for analytics.
652
+
653
+ ## Usage example
654
+
655
+ ${"```"}ts
656
+ import {
657
+ FlagEvaluator,
658
+ InMemoryFlagRepository,
659
+ } from '@contractspec/lib.feature-flags';
660
+
661
+ const repo = new InMemoryFlagRepository();
662
+ repo.addFlag({
663
+ id: 'flag-1',
664
+ key: 'new_dashboard',
665
+ status: 'GRADUAL',
666
+ defaultValue: false,
667
+ });
668
+
669
+ const evaluator = new FlagEvaluator({ repository: repo });
670
+ const result = await evaluator.evaluate('new_dashboard', {
671
+ userId: 'user-123',
672
+ orgId: 'org-456',
673
+ plan: 'pro',
674
+ });
675
+
676
+ if (result.enabled) {
677
+ // serve the new dashboard
678
+ }
679
+ ${"```"},
680
+
681
+ ## Guardrails
682
+
683
+ - Keep flag keys stable and human-readable; avoid PII in context.
684
+ - Ensure experiments\u2019 variant percentages sum to 100; default flag status to OFF.
685
+ - Use org-scoped flags for multi-tenant isolation.
686
+ - Log evaluations only when needed to control volume; prefer sampling for noisy paths.
687
+ `
688
+ }
689
+ ];
690
+ registerDocBlocks(featureFlagsDocBlocks);
691
+ // src/entities/index.ts
692
+ import {
693
+ defineEntity,
694
+ defineEntityEnum,
695
+ field,
696
+ index
697
+ } from "@contractspec/lib.schema";
698
+ var FlagStatusEnum = defineEntityEnum({
699
+ name: "FlagStatus",
700
+ values: ["OFF", "ON", "GRADUAL"],
701
+ schema: "lssm_feature_flags",
702
+ description: "Status of a feature flag."
703
+ });
704
+ var RuleOperatorEnum = defineEntityEnum({
705
+ name: "RuleOperator",
706
+ values: [
707
+ "EQ",
708
+ "NEQ",
709
+ "IN",
710
+ "NIN",
711
+ "CONTAINS",
712
+ "NOT_CONTAINS",
713
+ "GT",
714
+ "GTE",
715
+ "LT",
716
+ "LTE",
717
+ "PERCENTAGE"
718
+ ],
719
+ schema: "lssm_feature_flags",
720
+ description: "Operator for targeting rule conditions."
721
+ });
722
+ var ExperimentStatusEnum = defineEntityEnum({
723
+ name: "ExperimentStatus",
724
+ values: ["DRAFT", "RUNNING", "PAUSED", "COMPLETED", "CANCELLED"],
725
+ schema: "lssm_feature_flags",
726
+ description: "Status of an experiment."
727
+ });
728
+ var FeatureFlagEntity = defineEntity({
729
+ name: "FeatureFlag",
730
+ description: "A feature flag for controlling feature availability.",
731
+ schema: "lssm_feature_flags",
732
+ map: "feature_flag",
733
+ fields: {
734
+ id: field.id({ description: "Unique flag identifier" }),
735
+ key: field.string({
736
+ isUnique: true,
737
+ description: "Flag key (e.g., new_dashboard)"
738
+ }),
739
+ name: field.string({ description: "Human-readable name" }),
740
+ description: field.string({
741
+ isOptional: true,
742
+ description: "Description of the flag"
743
+ }),
744
+ status: field.enum("FlagStatus", {
745
+ default: "OFF",
746
+ description: "Flag status"
747
+ }),
748
+ defaultValue: field.boolean({
749
+ default: false,
750
+ description: "Default value when no rules match"
751
+ }),
752
+ variants: field.json({
753
+ isOptional: true,
754
+ description: "Variant definitions for multivariate flags"
755
+ }),
756
+ orgId: field.string({
757
+ isOptional: true,
758
+ description: "Organization scope (null = global)"
759
+ }),
760
+ tags: field.json({
761
+ isOptional: true,
762
+ description: "Tags for categorization"
763
+ }),
764
+ metadata: field.json({
765
+ isOptional: true,
766
+ description: "Additional metadata"
767
+ }),
768
+ createdAt: field.createdAt(),
769
+ updatedAt: field.updatedAt(),
770
+ targetingRules: field.hasMany("FlagTargetingRule"),
771
+ experiments: field.hasMany("Experiment"),
772
+ evaluations: field.hasMany("FlagEvaluation")
773
+ },
774
+ indexes: [index.on(["orgId", "key"]), index.on(["status"])],
775
+ enums: [FlagStatusEnum]
776
+ });
777
+ var FlagTargetingRuleEntity = defineEntity({
778
+ name: "FlagTargetingRule",
779
+ description: "A targeting rule for conditional flag evaluation.",
780
+ schema: "lssm_feature_flags",
781
+ map: "flag_targeting_rule",
782
+ fields: {
783
+ id: field.id({ description: "Unique rule identifier" }),
784
+ flagId: field.foreignKey({ description: "Parent feature flag" }),
785
+ name: field.string({
786
+ isOptional: true,
787
+ description: "Rule name for debugging"
788
+ }),
789
+ priority: field.int({
790
+ default: 0,
791
+ description: "Rule priority (lower = higher priority)"
792
+ }),
793
+ enabled: field.boolean({
794
+ default: true,
795
+ description: "Whether rule is active"
796
+ }),
797
+ attribute: field.string({
798
+ description: "Target attribute (userId, orgId, plan, segment, etc.)"
799
+ }),
800
+ operator: field.enum("RuleOperator", {
801
+ description: "Comparison operator"
802
+ }),
803
+ value: field.json({ description: "Target value(s)" }),
804
+ rolloutPercentage: field.int({
805
+ isOptional: true,
806
+ description: "Percentage for gradual rollout (0-100)"
807
+ }),
808
+ serveValue: field.boolean({
809
+ isOptional: true,
810
+ description: "Boolean value to serve"
811
+ }),
812
+ serveVariant: field.string({
813
+ isOptional: true,
814
+ description: "Variant key to serve (for multivariate)"
815
+ }),
816
+ createdAt: field.createdAt(),
817
+ updatedAt: field.updatedAt(),
818
+ flag: field.belongsTo("FeatureFlag", ["flagId"], ["id"], {
819
+ onDelete: "Cascade"
820
+ })
821
+ },
822
+ indexes: [index.on(["flagId", "priority"]), index.on(["attribute"])],
823
+ enums: [RuleOperatorEnum]
824
+ });
825
+ var ExperimentEntity = defineEntity({
826
+ name: "Experiment",
827
+ description: "An A/B test experiment.",
828
+ schema: "lssm_feature_flags",
829
+ map: "experiment",
830
+ fields: {
831
+ id: field.id({ description: "Unique experiment identifier" }),
832
+ key: field.string({ isUnique: true, description: "Experiment key" }),
833
+ name: field.string({ description: "Human-readable name" }),
834
+ description: field.string({
835
+ isOptional: true,
836
+ description: "Experiment description"
837
+ }),
838
+ hypothesis: field.string({
839
+ isOptional: true,
840
+ description: "Experiment hypothesis"
841
+ }),
842
+ flagId: field.foreignKey({ description: "Associated feature flag" }),
843
+ status: field.enum("ExperimentStatus", {
844
+ default: "DRAFT",
845
+ description: "Experiment status"
846
+ }),
847
+ variants: field.json({
848
+ description: "Variant definitions with split ratios"
849
+ }),
850
+ metrics: field.json({ isOptional: true, description: "Metrics to track" }),
851
+ audiencePercentage: field.int({
852
+ default: 100,
853
+ description: "Percentage of audience to include"
854
+ }),
855
+ audienceFilter: field.json({
856
+ isOptional: true,
857
+ description: "Audience filter criteria"
858
+ }),
859
+ scheduledStartAt: field.dateTime({
860
+ isOptional: true,
861
+ description: "Scheduled start time"
862
+ }),
863
+ scheduledEndAt: field.dateTime({
864
+ isOptional: true,
865
+ description: "Scheduled end time"
866
+ }),
867
+ startedAt: field.dateTime({
868
+ isOptional: true,
869
+ description: "Actual start time"
870
+ }),
871
+ endedAt: field.dateTime({
872
+ isOptional: true,
873
+ description: "Actual end time"
874
+ }),
875
+ winningVariant: field.string({
876
+ isOptional: true,
877
+ description: "Declared winning variant"
878
+ }),
879
+ results: field.json({
880
+ isOptional: true,
881
+ description: "Experiment results summary"
882
+ }),
883
+ orgId: field.string({
884
+ isOptional: true,
885
+ description: "Organization scope"
886
+ }),
887
+ createdAt: field.createdAt(),
888
+ updatedAt: field.updatedAt(),
889
+ flag: field.belongsTo("FeatureFlag", ["flagId"], ["id"], {
890
+ onDelete: "Cascade"
891
+ }),
892
+ assignments: field.hasMany("ExperimentAssignment")
893
+ },
894
+ indexes: [
895
+ index.on(["status"]),
896
+ index.on(["orgId", "status"]),
897
+ index.on(["flagId"])
898
+ ],
899
+ enums: [ExperimentStatusEnum]
900
+ });
901
+ var ExperimentAssignmentEntity = defineEntity({
902
+ name: "ExperimentAssignment",
903
+ description: "Tracks experiment variant assignments.",
904
+ schema: "lssm_feature_flags",
905
+ map: "experiment_assignment",
906
+ fields: {
907
+ id: field.id({ description: "Unique assignment identifier" }),
908
+ experimentId: field.foreignKey({ description: "Parent experiment" }),
909
+ subjectType: field.string({
910
+ description: "Subject type (user, org, session)"
911
+ }),
912
+ subjectId: field.string({ description: "Subject identifier" }),
913
+ variant: field.string({ description: "Assigned variant key" }),
914
+ bucket: field.int({ description: "Hash bucket (0-99)" }),
915
+ context: field.json({
916
+ isOptional: true,
917
+ description: "Context at assignment time"
918
+ }),
919
+ assignedAt: field.dateTime({ description: "Assignment timestamp" }),
920
+ experiment: field.belongsTo("Experiment", ["experimentId"], ["id"], {
921
+ onDelete: "Cascade"
922
+ })
923
+ },
924
+ indexes: [
925
+ index.unique(["experimentId", "subjectType", "subjectId"], {
926
+ name: "experiment_assignment_unique"
927
+ }),
928
+ index.on(["subjectType", "subjectId"])
929
+ ]
930
+ });
931
+ var FlagEvaluationEntity = defineEntity({
932
+ name: "FlagEvaluation",
933
+ description: "Log of flag evaluations for debugging and analytics.",
934
+ schema: "lssm_feature_flags",
935
+ map: "flag_evaluation",
936
+ fields: {
937
+ id: field.id({ description: "Unique evaluation identifier" }),
938
+ flagId: field.foreignKey({ description: "Evaluated flag" }),
939
+ flagKey: field.string({
940
+ description: "Flag key (denormalized for queries)"
941
+ }),
942
+ subjectType: field.string({
943
+ description: "Subject type (user, org, anonymous)"
944
+ }),
945
+ subjectId: field.string({ description: "Subject identifier" }),
946
+ result: field.boolean({ description: "Evaluation result" }),
947
+ variant: field.string({
948
+ isOptional: true,
949
+ description: "Served variant (for multivariate)"
950
+ }),
951
+ matchedRuleId: field.string({
952
+ isOptional: true,
953
+ description: "Rule that matched (if any)"
954
+ }),
955
+ reason: field.string({
956
+ description: "Evaluation reason (default, rule, experiment, etc.)"
957
+ }),
958
+ context: field.json({
959
+ isOptional: true,
960
+ description: "Evaluation context"
961
+ }),
962
+ evaluatedAt: field.dateTime({ description: "Evaluation timestamp" }),
963
+ flag: field.belongsTo("FeatureFlag", ["flagId"], ["id"], {
964
+ onDelete: "Cascade"
965
+ })
966
+ },
967
+ indexes: [
968
+ index.on(["flagKey", "evaluatedAt"]),
969
+ index.on(["subjectType", "subjectId", "evaluatedAt"]),
970
+ index.on(["flagId", "evaluatedAt"])
971
+ ]
972
+ });
973
+ var featureFlagEntities = [
974
+ FeatureFlagEntity,
975
+ FlagTargetingRuleEntity,
976
+ ExperimentEntity,
977
+ ExperimentAssignmentEntity,
978
+ FlagEvaluationEntity
979
+ ];
980
+ var featureFlagsSchemaContribution = {
981
+ moduleId: "@contractspec/lib.feature-flags",
982
+ entities: featureFlagEntities,
983
+ enums: [FlagStatusEnum, RuleOperatorEnum, ExperimentStatusEnum]
984
+ };
985
+
986
+ // src/evaluation/index.ts
987
+ function hashToBucket(value, seed = "") {
988
+ const input = `${seed}:${value}`;
989
+ let hash = 0;
990
+ for (let i = 0;i < input.length; i++) {
991
+ const char = input.charCodeAt(i);
992
+ hash = (hash << 5) - hash + char;
993
+ hash = hash & hash;
994
+ }
995
+ return Math.abs(hash % 100);
996
+ }
997
+ function getSubjectId(context) {
998
+ return context.userId || context.sessionId || context.orgId || "anonymous";
999
+ }
1000
+ function evaluateRuleCondition(rule, context) {
1001
+ const attributeValue = getAttributeValue(rule.attribute, context);
1002
+ switch (rule.operator) {
1003
+ case "EQ":
1004
+ return attributeValue === rule.value;
1005
+ case "NEQ":
1006
+ return attributeValue !== rule.value;
1007
+ case "IN":
1008
+ if (!Array.isArray(rule.value))
1009
+ return false;
1010
+ return rule.value.includes(attributeValue);
1011
+ case "NIN":
1012
+ if (!Array.isArray(rule.value))
1013
+ return true;
1014
+ return !rule.value.includes(attributeValue);
1015
+ case "CONTAINS":
1016
+ if (typeof attributeValue !== "string" || typeof rule.value !== "string")
1017
+ return false;
1018
+ return attributeValue.includes(rule.value);
1019
+ case "NOT_CONTAINS":
1020
+ if (typeof attributeValue !== "string" || typeof rule.value !== "string")
1021
+ return true;
1022
+ return !attributeValue.includes(rule.value);
1023
+ case "GT":
1024
+ if (typeof attributeValue !== "number" || typeof rule.value !== "number")
1025
+ return false;
1026
+ return attributeValue > rule.value;
1027
+ case "GTE":
1028
+ if (typeof attributeValue !== "number" || typeof rule.value !== "number")
1029
+ return false;
1030
+ return attributeValue >= rule.value;
1031
+ case "LT":
1032
+ if (typeof attributeValue !== "number" || typeof rule.value !== "number")
1033
+ return false;
1034
+ return attributeValue < rule.value;
1035
+ case "LTE":
1036
+ if (typeof attributeValue !== "number" || typeof rule.value !== "number")
1037
+ return false;
1038
+ return attributeValue <= rule.value;
1039
+ case "PERCENTAGE":
1040
+ return hashToBucket(getSubjectId(context), rule.attribute) < (typeof rule.value === "number" ? rule.value : 0);
1041
+ default:
1042
+ return false;
1043
+ }
1044
+ }
1045
+ function getAttributeValue(attribute, context) {
1046
+ switch (attribute) {
1047
+ case "userId":
1048
+ return context.userId;
1049
+ case "orgId":
1050
+ return context.orgId;
1051
+ case "plan":
1052
+ return context.plan;
1053
+ case "segment":
1054
+ return context.segment;
1055
+ case "sessionId":
1056
+ return context.sessionId;
1057
+ default:
1058
+ return context.attributes?.[attribute];
1059
+ }
1060
+ }
1061
+
1062
+ class FlagEvaluator {
1063
+ repository;
1064
+ logger;
1065
+ logEvaluations;
1066
+ constructor(options) {
1067
+ this.repository = options.repository;
1068
+ this.logger = options.logger;
1069
+ this.logEvaluations = options.logEvaluations ?? false;
1070
+ }
1071
+ async evaluate(key, context) {
1072
+ const orgId = context.orgId;
1073
+ const flag = await this.repository.getFlag(key, orgId);
1074
+ if (!flag) {
1075
+ return this.makeResult(false, "FLAG_NOT_FOUND");
1076
+ }
1077
+ if (flag.status === "OFF") {
1078
+ return this.logAndReturn(flag, context, this.makeResult(false, "FLAG_OFF"));
1079
+ }
1080
+ if (flag.status === "ON") {
1081
+ return this.logAndReturn(flag, context, this.makeResult(true, "FLAG_ON"));
1082
+ }
1083
+ const rules = await this.repository.getRules(flag.id);
1084
+ const sortedRules = [...rules].filter((r) => r.enabled).sort((a, b) => a.priority - b.priority);
1085
+ for (const rule of sortedRules) {
1086
+ if (evaluateRuleCondition(rule, context)) {
1087
+ if (rule.rolloutPercentage !== undefined && rule.rolloutPercentage !== null) {
1088
+ const bucket = hashToBucket(getSubjectId(context), flag.key);
1089
+ if (bucket >= rule.rolloutPercentage) {
1090
+ continue;
1091
+ }
1092
+ }
1093
+ const enabled = rule.serveValue ?? true;
1094
+ return this.logAndReturn(flag, context, this.makeResult(enabled, "RULE_MATCH", rule.serveVariant, rule.id));
1095
+ }
1096
+ }
1097
+ const experiment = await this.repository.getActiveExperiment(flag.id);
1098
+ if (experiment && experiment.status === "RUNNING") {
1099
+ const result = await this.evaluateExperiment(experiment, context);
1100
+ if (result) {
1101
+ return this.logAndReturn(flag, context, result);
1102
+ }
1103
+ }
1104
+ return this.logAndReturn(flag, context, this.makeResult(flag.defaultValue, "DEFAULT_VALUE"));
1105
+ }
1106
+ async evaluateExperiment(experiment, context) {
1107
+ const subjectId = getSubjectId(context);
1108
+ const subjectType = context.userId ? "user" : context.orgId ? "org" : "session";
1109
+ const audienceBucket = hashToBucket(subjectId, `${experiment.key}:audience`);
1110
+ if (audienceBucket >= experiment.audiencePercentage) {
1111
+ return null;
1112
+ }
1113
+ let variant = await this.repository.getExperimentAssignment(experiment.id, subjectType, subjectId);
1114
+ if (!variant) {
1115
+ const variantBucket = hashToBucket(subjectId, experiment.key);
1116
+ variant = this.assignVariant(experiment.variants, variantBucket);
1117
+ await this.repository.saveExperimentAssignment(experiment.id, subjectType, subjectId, variant, variantBucket);
1118
+ }
1119
+ const enabled = variant !== "control";
1120
+ return this.makeResult(enabled, "EXPERIMENT_VARIANT", variant, undefined, experiment.id);
1121
+ }
1122
+ assignVariant(variants, bucket) {
1123
+ let cumulative = 0;
1124
+ for (const variant of variants) {
1125
+ cumulative += variant.percentage;
1126
+ if (bucket < cumulative) {
1127
+ return variant.key;
1128
+ }
1129
+ }
1130
+ return variants[variants.length - 1]?.key ?? "control";
1131
+ }
1132
+ makeResult(enabled, reason, variant, ruleId, experimentId) {
1133
+ return {
1134
+ enabled,
1135
+ variant,
1136
+ reason,
1137
+ ruleId,
1138
+ experimentId
1139
+ };
1140
+ }
1141
+ logAndReturn(flag, context, result) {
1142
+ if (this.logEvaluations && this.logger) {
1143
+ const subjectId = getSubjectId(context);
1144
+ const subjectType = context.userId ? "user" : context.orgId ? "org" : "session";
1145
+ this.logger.log({
1146
+ flagId: flag.id,
1147
+ flagKey: flag.key,
1148
+ subjectType,
1149
+ subjectId,
1150
+ result: result.enabled,
1151
+ variant: result.variant,
1152
+ reason: result.reason,
1153
+ ruleId: result.ruleId,
1154
+ experimentId: result.experimentId,
1155
+ context
1156
+ });
1157
+ }
1158
+ return result;
1159
+ }
1160
+ }
1161
+
1162
+ class InMemoryFlagRepository {
1163
+ flags = new Map;
1164
+ rules = new Map;
1165
+ experiments = new Map;
1166
+ assignments = new Map;
1167
+ addFlag(flag) {
1168
+ this.flags.set(flag.key, flag);
1169
+ }
1170
+ addRule(flagId, rule) {
1171
+ const existing = this.rules.get(flagId) || [];
1172
+ existing.push(rule);
1173
+ this.rules.set(flagId, existing);
1174
+ }
1175
+ addExperiment(experiment, flagId) {
1176
+ this.experiments.set(flagId, experiment);
1177
+ }
1178
+ async getFlag(key) {
1179
+ return this.flags.get(key) || null;
1180
+ }
1181
+ async getRules(flagId) {
1182
+ return this.rules.get(flagId) || [];
1183
+ }
1184
+ async getActiveExperiment(flagId) {
1185
+ return this.experiments.get(flagId) || null;
1186
+ }
1187
+ async getExperimentAssignment(experimentId, subjectType, subjectId) {
1188
+ const key = `${experimentId}:${subjectType}:${subjectId}`;
1189
+ return this.assignments.get(key) || null;
1190
+ }
1191
+ async saveExperimentAssignment(experimentId, subjectType, subjectId, variant) {
1192
+ const key = `${experimentId}:${subjectType}:${subjectId}`;
1193
+ this.assignments.set(key, variant);
1194
+ }
1195
+ clear() {
1196
+ this.flags.clear();
1197
+ this.rules.clear();
1198
+ this.experiments.clear();
1199
+ this.assignments.clear();
1200
+ }
1201
+ }
1202
+
1203
+ // src/events.ts
1204
+ import { ScalarTypeEnum as ScalarTypeEnum2, defineSchemaModel as defineSchemaModel2 } from "@contractspec/lib.schema";
1205
+ import { defineEvent } from "@contractspec/lib.contracts";
1206
+ var FlagCreatedPayload = defineSchemaModel2({
1207
+ name: "FlagCreatedEventPayload",
1208
+ description: "Payload when a feature flag is created",
1209
+ fields: {
1210
+ flagId: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1211
+ key: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1212
+ name: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1213
+ status: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1214
+ orgId: { type: ScalarTypeEnum2.String_unsecure(), isOptional: true },
1215
+ createdBy: { type: ScalarTypeEnum2.String_unsecure(), isOptional: true },
1216
+ createdAt: { type: ScalarTypeEnum2.DateTime(), isOptional: false }
1217
+ }
1218
+ });
1219
+ var FlagUpdatedPayload = defineSchemaModel2({
1220
+ name: "FlagUpdatedEventPayload",
1221
+ description: "Payload when a feature flag is updated",
1222
+ fields: {
1223
+ flagId: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1224
+ key: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1225
+ changes: { type: ScalarTypeEnum2.JSON(), isOptional: false },
1226
+ updatedBy: { type: ScalarTypeEnum2.String_unsecure(), isOptional: true },
1227
+ updatedAt: { type: ScalarTypeEnum2.DateTime(), isOptional: false }
1228
+ }
1229
+ });
1230
+ var FlagDeletedPayload = defineSchemaModel2({
1231
+ name: "FlagDeletedEventPayload",
1232
+ description: "Payload when a feature flag is deleted",
1233
+ fields: {
1234
+ flagId: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1235
+ key: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1236
+ deletedBy: { type: ScalarTypeEnum2.String_unsecure(), isOptional: true },
1237
+ deletedAt: { type: ScalarTypeEnum2.DateTime(), isOptional: false }
1238
+ }
1239
+ });
1240
+ var FlagToggledPayload = defineSchemaModel2({
1241
+ name: "FlagToggledEventPayload",
1242
+ description: "Payload when a feature flag status is toggled",
1243
+ fields: {
1244
+ flagId: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1245
+ key: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1246
+ previousStatus: {
1247
+ type: ScalarTypeEnum2.String_unsecure(),
1248
+ isOptional: false
1249
+ },
1250
+ newStatus: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1251
+ toggledBy: { type: ScalarTypeEnum2.String_unsecure(), isOptional: true },
1252
+ toggledAt: { type: ScalarTypeEnum2.DateTime(), isOptional: false }
1253
+ }
1254
+ });
1255
+ var RuleCreatedPayload = defineSchemaModel2({
1256
+ name: "RuleCreatedEventPayload",
1257
+ description: "Payload when a targeting rule is created",
1258
+ fields: {
1259
+ ruleId: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1260
+ flagId: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1261
+ flagKey: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1262
+ attribute: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1263
+ operator: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1264
+ createdAt: { type: ScalarTypeEnum2.DateTime(), isOptional: false }
1265
+ }
1266
+ });
1267
+ var RuleDeletedPayload = defineSchemaModel2({
1268
+ name: "RuleDeletedEventPayload",
1269
+ description: "Payload when a targeting rule is deleted",
1270
+ fields: {
1271
+ ruleId: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1272
+ flagId: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1273
+ flagKey: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1274
+ deletedAt: { type: ScalarTypeEnum2.DateTime(), isOptional: false }
1275
+ }
1276
+ });
1277
+ var ExperimentCreatedPayload = defineSchemaModel2({
1278
+ name: "ExperimentCreatedEventPayload",
1279
+ description: "Payload when an experiment is created",
1280
+ fields: {
1281
+ experimentId: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1282
+ key: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1283
+ name: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1284
+ flagId: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1285
+ variants: { type: ScalarTypeEnum2.JSON(), isOptional: false },
1286
+ createdBy: { type: ScalarTypeEnum2.String_unsecure(), isOptional: true },
1287
+ createdAt: { type: ScalarTypeEnum2.DateTime(), isOptional: false }
1288
+ }
1289
+ });
1290
+ var ExperimentStartedPayload = defineSchemaModel2({
1291
+ name: "ExperimentStartedEventPayload",
1292
+ description: "Payload when an experiment starts",
1293
+ fields: {
1294
+ experimentId: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1295
+ key: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1296
+ flagId: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1297
+ variants: { type: ScalarTypeEnum2.JSON(), isOptional: false },
1298
+ audiencePercentage: {
1299
+ type: ScalarTypeEnum2.Int_unsecure(),
1300
+ isOptional: false
1301
+ },
1302
+ startedBy: { type: ScalarTypeEnum2.String_unsecure(), isOptional: true },
1303
+ startedAt: { type: ScalarTypeEnum2.DateTime(), isOptional: false }
1304
+ }
1305
+ });
1306
+ var ExperimentStoppedPayload = defineSchemaModel2({
1307
+ name: "ExperimentStoppedEventPayload",
1308
+ description: "Payload when an experiment stops",
1309
+ fields: {
1310
+ experimentId: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1311
+ key: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1312
+ reason: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1313
+ winningVariant: {
1314
+ type: ScalarTypeEnum2.String_unsecure(),
1315
+ isOptional: true
1316
+ },
1317
+ stoppedBy: { type: ScalarTypeEnum2.String_unsecure(), isOptional: true },
1318
+ stoppedAt: { type: ScalarTypeEnum2.DateTime(), isOptional: false }
1319
+ }
1320
+ });
1321
+ var FlagEvaluatedPayload = defineSchemaModel2({
1322
+ name: "FlagEvaluatedEventPayload",
1323
+ description: "Payload when a flag is evaluated (for analytics)",
1324
+ fields: {
1325
+ flagId: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1326
+ flagKey: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1327
+ subjectType: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1328
+ subjectId: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1329
+ result: { type: ScalarTypeEnum2.Boolean(), isOptional: false },
1330
+ variant: { type: ScalarTypeEnum2.String_unsecure(), isOptional: true },
1331
+ reason: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1332
+ evaluatedAt: { type: ScalarTypeEnum2.DateTime(), isOptional: false }
1333
+ }
1334
+ });
1335
+ var VariantAssignedPayload = defineSchemaModel2({
1336
+ name: "VariantAssignedEventPayload",
1337
+ description: "Payload when a subject is assigned to an experiment variant",
1338
+ fields: {
1339
+ experimentId: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1340
+ experimentKey: {
1341
+ type: ScalarTypeEnum2.String_unsecure(),
1342
+ isOptional: false
1343
+ },
1344
+ subjectType: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1345
+ subjectId: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1346
+ variant: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
1347
+ bucket: { type: ScalarTypeEnum2.Int_unsecure(), isOptional: false },
1348
+ assignedAt: { type: ScalarTypeEnum2.DateTime(), isOptional: false }
1349
+ }
1350
+ });
1351
+ var FlagCreatedEvent = defineEvent({
1352
+ meta: {
1353
+ key: "flag.created",
1354
+ version: "1.0.0",
1355
+ description: "A feature flag has been created.",
1356
+ stability: "stable",
1357
+ owners: ["@platform.feature-flags"],
1358
+ tags: ["feature-flags", "create"]
1359
+ },
1360
+ payload: FlagCreatedPayload
1361
+ });
1362
+ var FlagUpdatedEvent = defineEvent({
1363
+ meta: {
1364
+ key: "flag.updated",
1365
+ version: "1.0.0",
1366
+ description: "A feature flag has been updated.",
1367
+ stability: "stable",
1368
+ owners: ["@platform.feature-flags"],
1369
+ tags: ["feature-flags", "update"]
1370
+ },
1371
+ payload: FlagUpdatedPayload
1372
+ });
1373
+ var FlagDeletedEvent = defineEvent({
1374
+ meta: {
1375
+ key: "flag.deleted",
1376
+ version: "1.0.0",
1377
+ description: "A feature flag has been deleted.",
1378
+ stability: "stable",
1379
+ owners: ["@platform.feature-flags"],
1380
+ tags: ["feature-flags", "delete"]
1381
+ },
1382
+ payload: FlagDeletedPayload
1383
+ });
1384
+ var FlagToggledEvent = defineEvent({
1385
+ meta: {
1386
+ key: "flag.toggled",
1387
+ version: "1.0.0",
1388
+ description: "A feature flag status has been toggled.",
1389
+ stability: "stable",
1390
+ owners: ["@platform.feature-flags"],
1391
+ tags: ["feature-flags", "toggle"]
1392
+ },
1393
+ payload: FlagToggledPayload
1394
+ });
1395
+ var RuleCreatedEvent = defineEvent({
1396
+ meta: {
1397
+ key: "flag.rule_created",
1398
+ version: "1.0.0",
1399
+ description: "A targeting rule has been created.",
1400
+ stability: "stable",
1401
+ owners: ["@platform.feature-flags"],
1402
+ tags: ["feature-flags", "rule", "create"]
1403
+ },
1404
+ payload: RuleCreatedPayload
1405
+ });
1406
+ var RuleDeletedEvent = defineEvent({
1407
+ meta: {
1408
+ key: "flag.rule_deleted",
1409
+ version: "1.0.0",
1410
+ description: "A targeting rule has been deleted.",
1411
+ stability: "stable",
1412
+ owners: ["@platform.feature-flags"],
1413
+ tags: ["feature-flags", "rule", "delete"]
1414
+ },
1415
+ payload: RuleDeletedPayload
1416
+ });
1417
+ var ExperimentCreatedEvent = defineEvent({
1418
+ meta: {
1419
+ key: "experiment.created",
1420
+ version: "1.0.0",
1421
+ description: "An experiment has been created.",
1422
+ stability: "stable",
1423
+ owners: ["@platform.feature-flags"],
1424
+ tags: ["feature-flags", "experiment", "create"]
1425
+ },
1426
+ payload: ExperimentCreatedPayload
1427
+ });
1428
+ var ExperimentStartedEvent = defineEvent({
1429
+ meta: {
1430
+ key: "experiment.started",
1431
+ version: "1.0.0",
1432
+ description: "An experiment has started.",
1433
+ stability: "stable",
1434
+ owners: ["@platform.feature-flags"],
1435
+ tags: ["feature-flags", "experiment", "start"]
1436
+ },
1437
+ payload: ExperimentStartedPayload
1438
+ });
1439
+ var ExperimentStoppedEvent = defineEvent({
1440
+ meta: {
1441
+ key: "experiment.stopped",
1442
+ version: "1.0.0",
1443
+ description: "An experiment has stopped.",
1444
+ stability: "stable",
1445
+ owners: ["@platform.feature-flags"],
1446
+ tags: ["feature-flags", "experiment", "stop"]
1447
+ },
1448
+ payload: ExperimentStoppedPayload
1449
+ });
1450
+ var FlagEvaluatedEvent = defineEvent({
1451
+ meta: {
1452
+ key: "flag.evaluated",
1453
+ version: "1.0.0",
1454
+ description: "A feature flag has been evaluated.",
1455
+ stability: "stable",
1456
+ owners: ["@platform.feature-flags"],
1457
+ tags: ["feature-flags", "evaluate"]
1458
+ },
1459
+ payload: FlagEvaluatedPayload
1460
+ });
1461
+ var VariantAssignedEvent = defineEvent({
1462
+ meta: {
1463
+ key: "experiment.variant_assigned",
1464
+ version: "1.0.0",
1465
+ description: "A subject has been assigned to an experiment variant.",
1466
+ stability: "stable",
1467
+ owners: ["@platform.feature-flags"],
1468
+ tags: ["feature-flags", "experiment", "variant"]
1469
+ },
1470
+ payload: VariantAssignedPayload
1471
+ });
1472
+ var FeatureFlagEvents = {
1473
+ FlagCreatedEvent,
1474
+ FlagUpdatedEvent,
1475
+ FlagDeletedEvent,
1476
+ FlagToggledEvent,
1477
+ RuleCreatedEvent,
1478
+ RuleDeletedEvent,
1479
+ ExperimentCreatedEvent,
1480
+ ExperimentStartedEvent,
1481
+ ExperimentStoppedEvent,
1482
+ FlagEvaluatedEvent,
1483
+ VariantAssignedEvent
1484
+ };
1485
+
1486
+ // src/feature-flags.feature.ts
1487
+ import { defineFeature } from "@contractspec/lib.contracts";
1488
+ var FeatureFlagsFeature = defineFeature({
1489
+ meta: {
1490
+ key: "feature-flags",
1491
+ version: "1.0.0",
1492
+ title: "Feature Flags",
1493
+ description: "Feature flag management with targeting rules and A/B experiments",
1494
+ domain: "platform",
1495
+ owners: ["@platform.feature-flags"],
1496
+ tags: ["feature-flags", "experiments", "targeting"],
1497
+ stability: "stable"
1498
+ },
1499
+ operations: [
1500
+ { key: "flag.create", version: "1.0.0" },
1501
+ { key: "flag.update", version: "1.0.0" },
1502
+ { key: "flag.delete", version: "1.0.0" },
1503
+ { key: "flag.toggle", version: "1.0.0" },
1504
+ { key: "flag.get", version: "1.0.0" },
1505
+ { key: "flag.list", version: "1.0.0" },
1506
+ { key: "flag.evaluate", version: "1.0.0" },
1507
+ { key: "flag.rule.create", version: "1.0.0" },
1508
+ { key: "flag.rule.delete", version: "1.0.0" },
1509
+ { key: "experiment.create", version: "1.0.0" },
1510
+ { key: "experiment.start", version: "1.0.0" },
1511
+ { key: "experiment.stop", version: "1.0.0" },
1512
+ { key: "experiment.get", version: "1.0.0" }
1513
+ ],
1514
+ events: [
1515
+ { key: "flag.created", version: "1.0.0" },
1516
+ { key: "flag.updated", version: "1.0.0" },
1517
+ { key: "flag.deleted", version: "1.0.0" },
1518
+ { key: "flag.toggled", version: "1.0.0" },
1519
+ { key: "flag.evaluated", version: "1.0.0" },
1520
+ { key: "flag.rule_created", version: "1.0.0" },
1521
+ { key: "flag.rule_deleted", version: "1.0.0" },
1522
+ { key: "experiment.created", version: "1.0.0" },
1523
+ { key: "experiment.started", version: "1.0.0" },
1524
+ { key: "experiment.stopped", version: "1.0.0" },
1525
+ { key: "experiment.variant_assigned", version: "1.0.0" }
1526
+ ],
1527
+ presentations: [],
1528
+ opToPresentation: [],
1529
+ presentationsTargets: [],
1530
+ capabilities: {
1531
+ provides: [
1532
+ { key: "feature-flag", version: "1.0.0" },
1533
+ { key: "experiments", version: "1.0.0" }
1534
+ ],
1535
+ requires: [{ key: "identity", version: "1.0.0" }]
1536
+ }
1537
+ });
1538
+ export {
1539
+ hashToBucket,
1540
+ getSubjectId,
1541
+ featureFlagsSchemaContribution,
1542
+ featureFlagEntities,
1543
+ evaluateRuleCondition,
1544
+ VariantAssignedEvent,
1545
+ UpdateFlagContract,
1546
+ ToggleFlagContract,
1547
+ TargetingRuleModel,
1548
+ StopExperimentContract,
1549
+ StartExperimentContract,
1550
+ RuleOperatorEnum,
1551
+ RuleDeletedEvent,
1552
+ RuleCreatedEvent,
1553
+ ListFlagsContract,
1554
+ InMemoryFlagRepository,
1555
+ GetFlagContract,
1556
+ GetExperimentContract,
1557
+ FlagUpdatedEvent,
1558
+ FlagToggledEvent,
1559
+ FlagTargetingRuleEntity,
1560
+ FlagStatusEnum,
1561
+ FlagEvaluator,
1562
+ FlagEvaluationEntity,
1563
+ FlagEvaluatedEvent,
1564
+ FlagDeletedEvent,
1565
+ FlagCreatedEvent,
1566
+ FeatureFlagsFeature,
1567
+ FeatureFlagModel,
1568
+ FeatureFlagEvents,
1569
+ FeatureFlagEntity,
1570
+ ExperimentStoppedEvent,
1571
+ ExperimentStatusEnum,
1572
+ ExperimentStartedEvent,
1573
+ ExperimentModel,
1574
+ ExperimentEntity,
1575
+ ExperimentCreatedEvent,
1576
+ ExperimentAssignmentEntity,
1577
+ EvaluationResultModel,
1578
+ EvaluateFlagContract,
1579
+ DeleteRuleContract,
1580
+ DeleteFlagContract,
1581
+ CreateRuleContract,
1582
+ CreateFlagContract,
1583
+ CreateExperimentContract
1584
+ };