@contractspec/lib.feature-flags 3.7.19 → 3.7.21
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.
- package/dist/browser/feature-flags.feature.js +1 -1
- package/dist/browser/index.js +1 -1
- package/dist/feature-flags.feature.d.ts +0 -4
- package/dist/feature-flags.feature.js +1 -1
- package/dist/index.js +1 -1
- package/dist/node/feature-flags.feature.js +1 -1
- package/dist/node/index.js +1 -1
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
import{defineFeature as g}from"@contractspec/lib.contracts-spec";var j=g({meta:{key:"feature-flags",version:"1.0.0",title:"Feature Flags",description:"Feature
|
|
1
|
+
import{defineFeature as g}from"@contractspec/lib.contracts-spec/features";var j=g({meta:{key:"libs.feature-flags",version:"1.0.0",title:"Feature Flags",description:"Feature flags and experiments module for ContractSpec applications",domain:"feature-flags",owners:["@contractspec-core"],tags:["package","libs","feature-flags"],stability:"experimental"},operations:[{key:"flag.create",version:"1.0.0"},{key:"flag.update",version:"1.0.0"},{key:"flag.delete",version:"1.0.0"},{key:"flag.toggle",version:"1.0.0"},{key:"flag.get",version:"1.0.0"},{key:"flag.list",version:"1.0.0"},{key:"flag.evaluate",version:"1.0.0"},{key:"flag.rule.create",version:"1.0.0"},{key:"flag.rule.delete",version:"1.0.0"},{key:"experiment.create",version:"1.0.0"},{key:"experiment.start",version:"1.0.0"},{key:"experiment.stop",version:"1.0.0"},{key:"experiment.get",version:"1.0.0"}],events:[{key:"flag.created",version:"1.0.0"},{key:"flag.updated",version:"1.0.0"},{key:"flag.deleted",version:"1.0.0"},{key:"flag.toggled",version:"1.0.0"},{key:"flag.rule_created",version:"1.0.0"},{key:"flag.rule_deleted",version:"1.0.0"},{key:"experiment.created",version:"1.0.0"},{key:"experiment.started",version:"1.0.0"},{key:"experiment.stopped",version:"1.0.0"},{key:"flag.evaluated",version:"1.0.0"},{key:"experiment.variant_assigned",version:"1.0.0"}]});export{j as FeatureFlagsFeature};
|
package/dist/browser/index.js
CHANGED
|
@@ -54,4 +54,4 @@ ${"```"},
|
|
|
54
54
|
- Ensure experiments’ variant percentages sum to 100; default flag status to OFF.
|
|
55
55
|
- Use org-scoped flags for multi-tenant isolation.
|
|
56
56
|
- Log evaluations only when needed to control volume; prefer sampling for noisy paths.
|
|
57
|
-
`}];i(c);import{defineEntity as P,defineEntityEnum as V,field as z,index as $}from"@contractspec/lib.schema";var R=V({name:"FlagStatus",values:["OFF","ON","GRADUAL"],schema:"lssm_feature_flags",description:"Status of a feature flag."}),F=V({name:"RuleOperator",values:["EQ","NEQ","IN","NIN","CONTAINS","NOT_CONTAINS","GT","GTE","LT","LTE","PERCENTAGE"],schema:"lssm_feature_flags",description:"Operator for targeting rule conditions."}),M=V({name:"ExperimentStatus",values:["DRAFT","RUNNING","PAUSED","COMPLETED","CANCELLED"],schema:"lssm_feature_flags",description:"Status of an experiment."}),r=P({name:"FeatureFlag",description:"A feature flag for controlling feature availability.",schema:"lssm_feature_flags",map:"feature_flag",fields:{id:z.id({description:"Unique flag identifier"}),key:z.string({isUnique:!0,description:"Flag key (e.g., new_dashboard)"}),name:z.string({description:"Human-readable name"}),description:z.string({isOptional:!0,description:"Description of the flag"}),status:z.enum("FlagStatus",{default:"OFF",description:"Flag status"}),defaultValue:z.boolean({default:!1,description:"Default value when no rules match"}),variants:z.json({isOptional:!0,description:"Variant definitions for multivariate flags"}),orgId:z.string({isOptional:!0,description:"Organization scope (null = global)"}),tags:z.json({isOptional:!0,description:"Tags for categorization"}),metadata:z.json({isOptional:!0,description:"Additional metadata"}),createdAt:z.createdAt(),updatedAt:z.updatedAt(),targetingRules:z.hasMany("FlagTargetingRule"),experiments:z.hasMany("Experiment"),evaluations:z.hasMany("FlagEvaluation")},indexes:[$.on(["orgId","key"]),$.on(["status"])],enums:[R]}),u=P({name:"FlagTargetingRule",description:"A targeting rule for conditional flag evaluation.",schema:"lssm_feature_flags",map:"flag_targeting_rule",fields:{id:z.id({description:"Unique rule identifier"}),flagId:z.foreignKey({description:"Parent feature flag"}),name:z.string({isOptional:!0,description:"Rule name for debugging"}),priority:z.int({default:0,description:"Rule priority (lower = higher priority)"}),enabled:z.boolean({default:!0,description:"Whether rule is active"}),attribute:z.string({description:"Target attribute (userId, orgId, plan, segment, etc.)"}),operator:z.enum("RuleOperator",{description:"Comparison operator"}),value:z.json({description:"Target value(s)"}),rolloutPercentage:z.int({isOptional:!0,description:"Percentage for gradual rollout (0-100)"}),serveValue:z.boolean({isOptional:!0,description:"Boolean value to serve"}),serveVariant:z.string({isOptional:!0,description:"Variant key to serve (for multivariate)"}),createdAt:z.createdAt(),updatedAt:z.updatedAt(),flag:z.belongsTo("FeatureFlag",["flagId"],["id"],{onDelete:"Cascade"})},indexes:[$.on(["flagId","priority"]),$.on(["attribute"])],enums:[F]}),t=P({name:"Experiment",description:"An A/B test experiment.",schema:"lssm_feature_flags",map:"experiment",fields:{id:z.id({description:"Unique experiment identifier"}),key:z.string({isUnique:!0,description:"Experiment key"}),name:z.string({description:"Human-readable name"}),description:z.string({isOptional:!0,description:"Experiment description"}),hypothesis:z.string({isOptional:!0,description:"Experiment hypothesis"}),flagId:z.foreignKey({description:"Associated feature flag"}),status:z.enum("ExperimentStatus",{default:"DRAFT",description:"Experiment status"}),variants:z.json({description:"Variant definitions with split ratios"}),metrics:z.json({isOptional:!0,description:"Metrics to track"}),audiencePercentage:z.int({default:100,description:"Percentage of audience to include"}),audienceFilter:z.json({isOptional:!0,description:"Audience filter criteria"}),scheduledStartAt:z.dateTime({isOptional:!0,description:"Scheduled start time"}),scheduledEndAt:z.dateTime({isOptional:!0,description:"Scheduled end time"}),startedAt:z.dateTime({isOptional:!0,description:"Actual start time"}),endedAt:z.dateTime({isOptional:!0,description:"Actual end time"}),winningVariant:z.string({isOptional:!0,description:"Declared winning variant"}),results:z.json({isOptional:!0,description:"Experiment results summary"}),orgId:z.string({isOptional:!0,description:"Organization scope"}),createdAt:z.createdAt(),updatedAt:z.updatedAt(),flag:z.belongsTo("FeatureFlag",["flagId"],["id"],{onDelete:"Cascade"}),assignments:z.hasMany("ExperimentAssignment")},indexes:[$.on(["status"]),$.on(["orgId","status"]),$.on(["flagId"])],enums:[M]}),n=P({name:"ExperimentAssignment",description:"Tracks experiment variant assignments.",schema:"lssm_feature_flags",map:"experiment_assignment",fields:{id:z.id({description:"Unique assignment identifier"}),experimentId:z.foreignKey({description:"Parent experiment"}),subjectType:z.string({description:"Subject type (user, org, session)"}),subjectId:z.string({description:"Subject identifier"}),variant:z.string({description:"Assigned variant key"}),bucket:z.int({description:"Hash bucket (0-99)"}),context:z.json({isOptional:!0,description:"Context at assignment time"}),assignedAt:z.dateTime({description:"Assignment timestamp"}),experiment:z.belongsTo("Experiment",["experimentId"],["id"],{onDelete:"Cascade"})},indexes:[$.unique(["experimentId","subjectType","subjectId"],{name:"experiment_assignment_unique"}),$.on(["subjectType","subjectId"])]}),l=P({name:"FlagEvaluation",description:"Log of flag evaluations for debugging and analytics.",schema:"lssm_feature_flags",map:"flag_evaluation",fields:{id:z.id({description:"Unique evaluation identifier"}),flagId:z.foreignKey({description:"Evaluated flag"}),flagKey:z.string({description:"Flag key (denormalized for queries)"}),subjectType:z.string({description:"Subject type (user, org, anonymous)"}),subjectId:z.string({description:"Subject identifier"}),result:z.boolean({description:"Evaluation result"}),variant:z.string({isOptional:!0,description:"Served variant (for multivariate)"}),matchedRuleId:z.string({isOptional:!0,description:"Rule that matched (if any)"}),reason:z.string({description:"Evaluation reason (default, rule, experiment, etc.)"}),context:z.json({isOptional:!0,description:"Evaluation context"}),evaluatedAt:z.dateTime({description:"Evaluation timestamp"}),flag:z.belongsTo("FeatureFlag",["flagId"],["id"],{onDelete:"Cascade"})},indexes:[$.on(["flagKey","evaluatedAt"]),$.on(["subjectType","subjectId","evaluatedAt"]),$.on(["flagId","evaluatedAt"])]}),a=[r,u,t,n,l],cq={moduleId:"@contractspec/lib.feature-flags",entities:a,enums:[R,F,M]};function k(w,K=""){let J=`${K}:${w}`,X=0;for(let G=0;G<J.length;G++){let A=J.charCodeAt(G);X=(X<<5)-X+A,X=X&X}return Math.abs(X%100)}function C(w){return w.userId||w.sessionId||w.orgId||"anonymous"}function e(w,K){let J=qq(w.attribute,K);switch(w.operator){case"EQ":return J===w.value;case"NEQ":return J!==w.value;case"IN":if(!Array.isArray(w.value))return!1;return w.value.includes(J);case"NIN":if(!Array.isArray(w.value))return!0;return!w.value.includes(J);case"CONTAINS":if(typeof J!=="string"||typeof w.value!=="string")return!1;return J.includes(w.value);case"NOT_CONTAINS":if(typeof J!=="string"||typeof w.value!=="string")return!0;return!J.includes(w.value);case"GT":if(typeof J!=="number"||typeof w.value!=="number")return!1;return J>w.value;case"GTE":if(typeof J!=="number"||typeof w.value!=="number")return!1;return J>=w.value;case"LT":if(typeof J!=="number"||typeof w.value!=="number")return!1;return J<w.value;case"LTE":if(typeof J!=="number"||typeof w.value!=="number")return!1;return J<=w.value;case"PERCENTAGE":return k(C(K),w.attribute)<(typeof w.value==="number"?w.value:0);default:return!1}}function qq(w,K){switch(w){case"userId":return K.userId;case"orgId":return K.orgId;case"plan":return K.plan;case"segment":return K.segment;case"sessionId":return K.sessionId;default:return K.attributes?.[w]}}class wq{repository;logger;logEvaluations;constructor(w){this.repository=w.repository,this.logger=w.logger,this.logEvaluations=w.logEvaluations??!1}async evaluate(w,K){let J=K.orgId,X=await this.repository.getFlag(w,J);if(!X)return this.makeResult(!1,"FLAG_NOT_FOUND");if(X.status==="OFF")return this.logAndReturn(X,K,this.makeResult(!1,"FLAG_OFF"));if(X.status==="ON")return this.logAndReturn(X,K,this.makeResult(!0,"FLAG_ON"));let A=[...await this.repository.getRules(X.id)].filter((Z)=>Z.enabled).sort((Z,O)=>Z.priority-O.priority);for(let Z of A)if(e(Z,K)){if(Z.rolloutPercentage!==void 0&&Z.rolloutPercentage!==null){if(k(C(K),X.key)>=Z.rolloutPercentage)continue}let O=Z.serveValue??!0;return this.logAndReturn(X,K,this.makeResult(O,"RULE_MATCH",Z.serveVariant,Z.id))}let B=await this.repository.getActiveExperiment(X.id);if(B&&B.status==="RUNNING"){let Z=await this.evaluateExperiment(B,K);if(Z)return this.logAndReturn(X,K,Z)}return this.logAndReturn(X,K,this.makeResult(X.defaultValue,"DEFAULT_VALUE"))}async evaluateExperiment(w,K){let J=C(K),X=K.userId?"user":K.orgId?"org":"session";if(k(J,`${w.key}:audience`)>=w.audiencePercentage)return null;let A=await this.repository.getExperimentAssignment(w.id,X,J);if(!A){let Z=k(J,w.key);A=this.assignVariant(w.variants,Z),await this.repository.saveExperimentAssignment(w.id,X,J,A,Z)}let B=A!=="control";return this.makeResult(B,"EXPERIMENT_VARIANT",A,void 0,w.id)}assignVariant(w,K){let J=0;for(let X of w)if(J+=X.percentage,K<J)return X.key;return w[w.length-1]?.key??"control"}makeResult(w,K,J,X,G){return{enabled:w,variant:J,reason:K,ruleId:X,experimentId:G}}logAndReturn(w,K,J){if(this.logEvaluations&&this.logger){let X=C(K),G=K.userId?"user":K.orgId?"org":"session";this.logger.log({flagId:w.id,flagKey:w.key,subjectType:G,subjectId:X,result:J.enabled,variant:J.variant,reason:J.reason,ruleId:J.ruleId,experimentId:J.experimentId,context:K})}return J}}class zq{flags=new Map;rules=new Map;experiments=new Map;assignments=new Map;addFlag(w){this.flags.set(w.key,w)}addRule(w,K){let J=this.rules.get(w)||[];J.push(K),this.rules.set(w,J)}addExperiment(w,K){this.experiments.set(K,w)}async getFlag(w){return this.flags.get(w)||null}async getRules(w){return this.rules.get(w)||[]}async getActiveExperiment(w){return this.experiments.get(w)||null}async getExperimentAssignment(w,K,J){let X=`${w}:${K}:${J}`;return this.assignments.get(X)||null}async saveExperimentAssignment(w,K,J,X){let G=`${w}:${K}:${J}`;this.assignments.set(G,X)}clear(){this.flags.clear(),this.rules.clear(),this.experiments.clear(),this.assignments.clear()}}import{defineEvent as L}from"@contractspec/lib.contracts-spec";import{defineSchemaModel as U,ScalarTypeEnum as H}from"@contractspec/lib.schema";var Hq=U({name:"FlagCreatedEventPayload",description:"Payload when a feature flag is created",fields:{flagId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},name:{type:H.String_unsecure(),isOptional:!1},status:{type:H.String_unsecure(),isOptional:!1},orgId:{type:H.String_unsecure(),isOptional:!0},createdBy:{type:H.String_unsecure(),isOptional:!0},createdAt:{type:H.DateTime(),isOptional:!1}}}),Jq=U({name:"FlagUpdatedEventPayload",description:"Payload when a feature flag is updated",fields:{flagId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},changes:{type:H.JSON(),isOptional:!1},updatedBy:{type:H.String_unsecure(),isOptional:!0},updatedAt:{type:H.DateTime(),isOptional:!1}}}),Kq=U({name:"FlagDeletedEventPayload",description:"Payload when a feature flag is deleted",fields:{flagId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},deletedBy:{type:H.String_unsecure(),isOptional:!0},deletedAt:{type:H.DateTime(),isOptional:!1}}}),Xq=U({name:"FlagToggledEventPayload",description:"Payload when a feature flag status is toggled",fields:{flagId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},previousStatus:{type:H.String_unsecure(),isOptional:!1},newStatus:{type:H.String_unsecure(),isOptional:!1},toggledBy:{type:H.String_unsecure(),isOptional:!0},toggledAt:{type:H.DateTime(),isOptional:!1}}}),Yq=U({name:"RuleCreatedEventPayload",description:"Payload when a targeting rule is created",fields:{ruleId:{type:H.String_unsecure(),isOptional:!1},flagId:{type:H.String_unsecure(),isOptional:!1},flagKey:{type:H.String_unsecure(),isOptional:!1},attribute:{type:H.String_unsecure(),isOptional:!1},operator:{type:H.String_unsecure(),isOptional:!1},createdAt:{type:H.DateTime(),isOptional:!1}}}),Zq=U({name:"RuleDeletedEventPayload",description:"Payload when a targeting rule is deleted",fields:{ruleId:{type:H.String_unsecure(),isOptional:!1},flagId:{type:H.String_unsecure(),isOptional:!1},flagKey:{type:H.String_unsecure(),isOptional:!1},deletedAt:{type:H.DateTime(),isOptional:!1}}}),_q=U({name:"ExperimentCreatedEventPayload",description:"Payload when an experiment is created",fields:{experimentId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},name:{type:H.String_unsecure(),isOptional:!1},flagId:{type:H.String_unsecure(),isOptional:!1},variants:{type:H.JSON(),isOptional:!1},createdBy:{type:H.String_unsecure(),isOptional:!0},createdAt:{type:H.DateTime(),isOptional:!1}}}),$q=U({name:"ExperimentStartedEventPayload",description:"Payload when an experiment starts",fields:{experimentId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},flagId:{type:H.String_unsecure(),isOptional:!1},variants:{type:H.JSON(),isOptional:!1},audiencePercentage:{type:H.Int_unsecure(),isOptional:!1},startedBy:{type:H.String_unsecure(),isOptional:!0},startedAt:{type:H.DateTime(),isOptional:!1}}}),Gq=U({name:"ExperimentStoppedEventPayload",description:"Payload when an experiment stops",fields:{experimentId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},reason:{type:H.String_unsecure(),isOptional:!1},winningVariant:{type:H.String_unsecure(),isOptional:!0},stoppedBy:{type:H.String_unsecure(),isOptional:!0},stoppedAt:{type:H.DateTime(),isOptional:!1}}}),Lq=U({name:"FlagEvaluatedEventPayload",description:"Payload when a flag is evaluated (for analytics)",fields:{flagId:{type:H.String_unsecure(),isOptional:!1},flagKey:{type:H.String_unsecure(),isOptional:!1},subjectType:{type:H.String_unsecure(),isOptional:!1},subjectId:{type:H.String_unsecure(),isOptional:!1},result:{type:H.Boolean(),isOptional:!1},variant:{type:H.String_unsecure(),isOptional:!0},reason:{type:H.String_unsecure(),isOptional:!1},evaluatedAt:{type:H.DateTime(),isOptional:!1}}}),Uq=U({name:"VariantAssignedEventPayload",description:"Payload when a subject is assigned to an experiment variant",fields:{experimentId:{type:H.String_unsecure(),isOptional:!1},experimentKey:{type:H.String_unsecure(),isOptional:!1},subjectType:{type:H.String_unsecure(),isOptional:!1},subjectId:{type:H.String_unsecure(),isOptional:!1},variant:{type:H.String_unsecure(),isOptional:!1},bucket:{type:H.Int_unsecure(),isOptional:!1},assignedAt:{type:H.DateTime(),isOptional:!1}}}),Aq=L({meta:{key:"flag.created",version:"1.0.0",description:"A feature flag has been created.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","create"]},payload:Hq}),Qq=L({meta:{key:"flag.updated",version:"1.0.0",description:"A feature flag has been updated.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","update"]},payload:Jq}),Bq=L({meta:{key:"flag.deleted",version:"1.0.0",description:"A feature flag has been deleted.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","delete"]},payload:Kq}),Dq=L({meta:{key:"flag.toggled",version:"1.0.0",description:"A feature flag status has been toggled.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","toggle"]},payload:Xq}),Pq=L({meta:{key:"flag.rule_created",version:"1.0.0",description:"A targeting rule has been created.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","rule","create"]},payload:Yq}),Nq=L({meta:{key:"flag.rule_deleted",version:"1.0.0",description:"A targeting rule has been deleted.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","rule","delete"]},payload:Zq}),Wq=L({meta:{key:"experiment.created",version:"1.0.0",description:"An experiment has been created.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","experiment","create"]},payload:_q}),kq=L({meta:{key:"experiment.started",version:"1.0.0",description:"An experiment has started.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","experiment","start"]},payload:$q}),Cq=L({meta:{key:"experiment.stopped",version:"1.0.0",description:"An experiment has stopped.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","experiment","stop"]},payload:Gq}),Oq=L({meta:{key:"flag.evaluated",version:"1.0.0",description:"A feature flag has been evaluated.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","evaluate"]},payload:Lq}),Vq=L({meta:{key:"experiment.variant_assigned",version:"1.0.0",description:"A subject has been assigned to an experiment variant.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","experiment","variant"]},payload:Uq}),lq={FlagCreatedEvent:Aq,FlagUpdatedEvent:Qq,FlagDeletedEvent:Bq,FlagToggledEvent:Dq,RuleCreatedEvent:Pq,RuleDeletedEvent:Nq,ExperimentCreatedEvent:Wq,ExperimentStartedEvent:kq,ExperimentStoppedEvent:Cq,FlagEvaluatedEvent:Oq,VariantAssignedEvent:Vq};import{defineFeature as jq}from"@contractspec/lib.contracts-spec";var qw=jq({meta:{key:"feature-flags",version:"1.0.0",title:"Feature Flags",description:"Feature flag management with targeting rules and A/B experiments",domain:"platform",owners:["@platform.feature-flags"],tags:["feature-flags","experiments","targeting"],stability:"stable"},operations:[{key:"flag.create",version:"1.0.0"},{key:"flag.update",version:"1.0.0"},{key:"flag.delete",version:"1.0.0"},{key:"flag.toggle",version:"1.0.0"},{key:"flag.get",version:"1.0.0"},{key:"flag.list",version:"1.0.0"},{key:"flag.evaluate",version:"1.0.0"},{key:"flag.rule.create",version:"1.0.0"},{key:"flag.rule.delete",version:"1.0.0"},{key:"experiment.create",version:"1.0.0"},{key:"experiment.start",version:"1.0.0"},{key:"experiment.stop",version:"1.0.0"},{key:"experiment.get",version:"1.0.0"}],events:[{key:"flag.created",version:"1.0.0"},{key:"flag.updated",version:"1.0.0"},{key:"flag.deleted",version:"1.0.0"},{key:"flag.toggled",version:"1.0.0"},{key:"flag.evaluated",version:"1.0.0"},{key:"flag.rule_created",version:"1.0.0"},{key:"flag.rule_deleted",version:"1.0.0"},{key:"experiment.created",version:"1.0.0"},{key:"experiment.started",version:"1.0.0"},{key:"experiment.stopped",version:"1.0.0"},{key:"experiment.variant_assigned",version:"1.0.0"}],presentations:[],opToPresentation:[],presentationsTargets:[],capabilities:{provides:[{key:"feature-flag",version:"1.0.0"},{key:"experiments",version:"1.0.0"}],requires:[{key:"identity",version:"1.0.0"}]}});export{k as hashToBucket,C as getSubjectId,cq as featureFlagsSchemaContribution,a as featureFlagEntities,e as evaluateRuleCondition,Vq as VariantAssignedEvent,vq as UpdateFlagContract,bq as ToggleFlagContract,I as TargetingRuleModel,Sq as StopExperimentContract,yq as StartExperimentContract,F as RuleOperatorEnum,Nq as RuleDeletedEvent,Pq as RuleCreatedEvent,sq as ListFlagsContract,zq as InMemoryFlagRepository,gq as GetFlagContract,Eq as GetExperimentContract,Qq as FlagUpdatedEvent,Dq as FlagToggledEvent,u as FlagTargetingRuleEntity,R as FlagStatusEnum,wq as FlagEvaluator,l as FlagEvaluationEntity,Oq as FlagEvaluatedEvent,Bq as FlagDeletedEvent,Aq as FlagCreatedEvent,qw as FeatureFlagsFeature,D as FeatureFlagModel,lq as FeatureFlagEvents,r as FeatureFlagEntity,Cq as ExperimentStoppedEvent,M as ExperimentStatusEnum,kq as ExperimentStartedEvent,W as ExperimentModel,t as ExperimentEntity,Wq as ExperimentCreatedEvent,n as ExperimentAssignmentEntity,v as EvaluationResultModel,xq as EvaluateFlagContract,fq as DeleteRuleContract,hq as DeleteFlagContract,oq as CreateRuleContract,Iq as CreateFlagContract,Tq as CreateExperimentContract};
|
|
57
|
+
`}];i(c);import{defineEntity as P,defineEntityEnum as V,field as z,index as $}from"@contractspec/lib.schema";var R=V({name:"FlagStatus",values:["OFF","ON","GRADUAL"],schema:"lssm_feature_flags",description:"Status of a feature flag."}),F=V({name:"RuleOperator",values:["EQ","NEQ","IN","NIN","CONTAINS","NOT_CONTAINS","GT","GTE","LT","LTE","PERCENTAGE"],schema:"lssm_feature_flags",description:"Operator for targeting rule conditions."}),M=V({name:"ExperimentStatus",values:["DRAFT","RUNNING","PAUSED","COMPLETED","CANCELLED"],schema:"lssm_feature_flags",description:"Status of an experiment."}),r=P({name:"FeatureFlag",description:"A feature flag for controlling feature availability.",schema:"lssm_feature_flags",map:"feature_flag",fields:{id:z.id({description:"Unique flag identifier"}),key:z.string({isUnique:!0,description:"Flag key (e.g., new_dashboard)"}),name:z.string({description:"Human-readable name"}),description:z.string({isOptional:!0,description:"Description of the flag"}),status:z.enum("FlagStatus",{default:"OFF",description:"Flag status"}),defaultValue:z.boolean({default:!1,description:"Default value when no rules match"}),variants:z.json({isOptional:!0,description:"Variant definitions for multivariate flags"}),orgId:z.string({isOptional:!0,description:"Organization scope (null = global)"}),tags:z.json({isOptional:!0,description:"Tags for categorization"}),metadata:z.json({isOptional:!0,description:"Additional metadata"}),createdAt:z.createdAt(),updatedAt:z.updatedAt(),targetingRules:z.hasMany("FlagTargetingRule"),experiments:z.hasMany("Experiment"),evaluations:z.hasMany("FlagEvaluation")},indexes:[$.on(["orgId","key"]),$.on(["status"])],enums:[R]}),u=P({name:"FlagTargetingRule",description:"A targeting rule for conditional flag evaluation.",schema:"lssm_feature_flags",map:"flag_targeting_rule",fields:{id:z.id({description:"Unique rule identifier"}),flagId:z.foreignKey({description:"Parent feature flag"}),name:z.string({isOptional:!0,description:"Rule name for debugging"}),priority:z.int({default:0,description:"Rule priority (lower = higher priority)"}),enabled:z.boolean({default:!0,description:"Whether rule is active"}),attribute:z.string({description:"Target attribute (userId, orgId, plan, segment, etc.)"}),operator:z.enum("RuleOperator",{description:"Comparison operator"}),value:z.json({description:"Target value(s)"}),rolloutPercentage:z.int({isOptional:!0,description:"Percentage for gradual rollout (0-100)"}),serveValue:z.boolean({isOptional:!0,description:"Boolean value to serve"}),serveVariant:z.string({isOptional:!0,description:"Variant key to serve (for multivariate)"}),createdAt:z.createdAt(),updatedAt:z.updatedAt(),flag:z.belongsTo("FeatureFlag",["flagId"],["id"],{onDelete:"Cascade"})},indexes:[$.on(["flagId","priority"]),$.on(["attribute"])],enums:[F]}),t=P({name:"Experiment",description:"An A/B test experiment.",schema:"lssm_feature_flags",map:"experiment",fields:{id:z.id({description:"Unique experiment identifier"}),key:z.string({isUnique:!0,description:"Experiment key"}),name:z.string({description:"Human-readable name"}),description:z.string({isOptional:!0,description:"Experiment description"}),hypothesis:z.string({isOptional:!0,description:"Experiment hypothesis"}),flagId:z.foreignKey({description:"Associated feature flag"}),status:z.enum("ExperimentStatus",{default:"DRAFT",description:"Experiment status"}),variants:z.json({description:"Variant definitions with split ratios"}),metrics:z.json({isOptional:!0,description:"Metrics to track"}),audiencePercentage:z.int({default:100,description:"Percentage of audience to include"}),audienceFilter:z.json({isOptional:!0,description:"Audience filter criteria"}),scheduledStartAt:z.dateTime({isOptional:!0,description:"Scheduled start time"}),scheduledEndAt:z.dateTime({isOptional:!0,description:"Scheduled end time"}),startedAt:z.dateTime({isOptional:!0,description:"Actual start time"}),endedAt:z.dateTime({isOptional:!0,description:"Actual end time"}),winningVariant:z.string({isOptional:!0,description:"Declared winning variant"}),results:z.json({isOptional:!0,description:"Experiment results summary"}),orgId:z.string({isOptional:!0,description:"Organization scope"}),createdAt:z.createdAt(),updatedAt:z.updatedAt(),flag:z.belongsTo("FeatureFlag",["flagId"],["id"],{onDelete:"Cascade"}),assignments:z.hasMany("ExperimentAssignment")},indexes:[$.on(["status"]),$.on(["orgId","status"]),$.on(["flagId"])],enums:[M]}),n=P({name:"ExperimentAssignment",description:"Tracks experiment variant assignments.",schema:"lssm_feature_flags",map:"experiment_assignment",fields:{id:z.id({description:"Unique assignment identifier"}),experimentId:z.foreignKey({description:"Parent experiment"}),subjectType:z.string({description:"Subject type (user, org, session)"}),subjectId:z.string({description:"Subject identifier"}),variant:z.string({description:"Assigned variant key"}),bucket:z.int({description:"Hash bucket (0-99)"}),context:z.json({isOptional:!0,description:"Context at assignment time"}),assignedAt:z.dateTime({description:"Assignment timestamp"}),experiment:z.belongsTo("Experiment",["experimentId"],["id"],{onDelete:"Cascade"})},indexes:[$.unique(["experimentId","subjectType","subjectId"],{name:"experiment_assignment_unique"}),$.on(["subjectType","subjectId"])]}),l=P({name:"FlagEvaluation",description:"Log of flag evaluations for debugging and analytics.",schema:"lssm_feature_flags",map:"flag_evaluation",fields:{id:z.id({description:"Unique evaluation identifier"}),flagId:z.foreignKey({description:"Evaluated flag"}),flagKey:z.string({description:"Flag key (denormalized for queries)"}),subjectType:z.string({description:"Subject type (user, org, anonymous)"}),subjectId:z.string({description:"Subject identifier"}),result:z.boolean({description:"Evaluation result"}),variant:z.string({isOptional:!0,description:"Served variant (for multivariate)"}),matchedRuleId:z.string({isOptional:!0,description:"Rule that matched (if any)"}),reason:z.string({description:"Evaluation reason (default, rule, experiment, etc.)"}),context:z.json({isOptional:!0,description:"Evaluation context"}),evaluatedAt:z.dateTime({description:"Evaluation timestamp"}),flag:z.belongsTo("FeatureFlag",["flagId"],["id"],{onDelete:"Cascade"})},indexes:[$.on(["flagKey","evaluatedAt"]),$.on(["subjectType","subjectId","evaluatedAt"]),$.on(["flagId","evaluatedAt"])]}),a=[r,u,t,n,l],cq={moduleId:"@contractspec/lib.feature-flags",entities:a,enums:[R,F,M]};function k(w,K=""){let J=`${K}:${w}`,X=0;for(let G=0;G<J.length;G++){let A=J.charCodeAt(G);X=(X<<5)-X+A,X=X&X}return Math.abs(X%100)}function C(w){return w.userId||w.sessionId||w.orgId||"anonymous"}function e(w,K){let J=qq(w.attribute,K);switch(w.operator){case"EQ":return J===w.value;case"NEQ":return J!==w.value;case"IN":if(!Array.isArray(w.value))return!1;return w.value.includes(J);case"NIN":if(!Array.isArray(w.value))return!0;return!w.value.includes(J);case"CONTAINS":if(typeof J!=="string"||typeof w.value!=="string")return!1;return J.includes(w.value);case"NOT_CONTAINS":if(typeof J!=="string"||typeof w.value!=="string")return!0;return!J.includes(w.value);case"GT":if(typeof J!=="number"||typeof w.value!=="number")return!1;return J>w.value;case"GTE":if(typeof J!=="number"||typeof w.value!=="number")return!1;return J>=w.value;case"LT":if(typeof J!=="number"||typeof w.value!=="number")return!1;return J<w.value;case"LTE":if(typeof J!=="number"||typeof w.value!=="number")return!1;return J<=w.value;case"PERCENTAGE":return k(C(K),w.attribute)<(typeof w.value==="number"?w.value:0);default:return!1}}function qq(w,K){switch(w){case"userId":return K.userId;case"orgId":return K.orgId;case"plan":return K.plan;case"segment":return K.segment;case"sessionId":return K.sessionId;default:return K.attributes?.[w]}}class wq{repository;logger;logEvaluations;constructor(w){this.repository=w.repository,this.logger=w.logger,this.logEvaluations=w.logEvaluations??!1}async evaluate(w,K){let J=K.orgId,X=await this.repository.getFlag(w,J);if(!X)return this.makeResult(!1,"FLAG_NOT_FOUND");if(X.status==="OFF")return this.logAndReturn(X,K,this.makeResult(!1,"FLAG_OFF"));if(X.status==="ON")return this.logAndReturn(X,K,this.makeResult(!0,"FLAG_ON"));let A=[...await this.repository.getRules(X.id)].filter((Z)=>Z.enabled).sort((Z,O)=>Z.priority-O.priority);for(let Z of A)if(e(Z,K)){if(Z.rolloutPercentage!==void 0&&Z.rolloutPercentage!==null){if(k(C(K),X.key)>=Z.rolloutPercentage)continue}let O=Z.serveValue??!0;return this.logAndReturn(X,K,this.makeResult(O,"RULE_MATCH",Z.serveVariant,Z.id))}let B=await this.repository.getActiveExperiment(X.id);if(B&&B.status==="RUNNING"){let Z=await this.evaluateExperiment(B,K);if(Z)return this.logAndReturn(X,K,Z)}return this.logAndReturn(X,K,this.makeResult(X.defaultValue,"DEFAULT_VALUE"))}async evaluateExperiment(w,K){let J=C(K),X=K.userId?"user":K.orgId?"org":"session";if(k(J,`${w.key}:audience`)>=w.audiencePercentage)return null;let A=await this.repository.getExperimentAssignment(w.id,X,J);if(!A){let Z=k(J,w.key);A=this.assignVariant(w.variants,Z),await this.repository.saveExperimentAssignment(w.id,X,J,A,Z)}let B=A!=="control";return this.makeResult(B,"EXPERIMENT_VARIANT",A,void 0,w.id)}assignVariant(w,K){let J=0;for(let X of w)if(J+=X.percentage,K<J)return X.key;return w[w.length-1]?.key??"control"}makeResult(w,K,J,X,G){return{enabled:w,variant:J,reason:K,ruleId:X,experimentId:G}}logAndReturn(w,K,J){if(this.logEvaluations&&this.logger){let X=C(K),G=K.userId?"user":K.orgId?"org":"session";this.logger.log({flagId:w.id,flagKey:w.key,subjectType:G,subjectId:X,result:J.enabled,variant:J.variant,reason:J.reason,ruleId:J.ruleId,experimentId:J.experimentId,context:K})}return J}}class zq{flags=new Map;rules=new Map;experiments=new Map;assignments=new Map;addFlag(w){this.flags.set(w.key,w)}addRule(w,K){let J=this.rules.get(w)||[];J.push(K),this.rules.set(w,J)}addExperiment(w,K){this.experiments.set(K,w)}async getFlag(w){return this.flags.get(w)||null}async getRules(w){return this.rules.get(w)||[]}async getActiveExperiment(w){return this.experiments.get(w)||null}async getExperimentAssignment(w,K,J){let X=`${w}:${K}:${J}`;return this.assignments.get(X)||null}async saveExperimentAssignment(w,K,J,X){let G=`${w}:${K}:${J}`;this.assignments.set(G,X)}clear(){this.flags.clear(),this.rules.clear(),this.experiments.clear(),this.assignments.clear()}}import{defineEvent as L}from"@contractspec/lib.contracts-spec";import{defineSchemaModel as U,ScalarTypeEnum as H}from"@contractspec/lib.schema";var Hq=U({name:"FlagCreatedEventPayload",description:"Payload when a feature flag is created",fields:{flagId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},name:{type:H.String_unsecure(),isOptional:!1},status:{type:H.String_unsecure(),isOptional:!1},orgId:{type:H.String_unsecure(),isOptional:!0},createdBy:{type:H.String_unsecure(),isOptional:!0},createdAt:{type:H.DateTime(),isOptional:!1}}}),Jq=U({name:"FlagUpdatedEventPayload",description:"Payload when a feature flag is updated",fields:{flagId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},changes:{type:H.JSON(),isOptional:!1},updatedBy:{type:H.String_unsecure(),isOptional:!0},updatedAt:{type:H.DateTime(),isOptional:!1}}}),Kq=U({name:"FlagDeletedEventPayload",description:"Payload when a feature flag is deleted",fields:{flagId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},deletedBy:{type:H.String_unsecure(),isOptional:!0},deletedAt:{type:H.DateTime(),isOptional:!1}}}),Xq=U({name:"FlagToggledEventPayload",description:"Payload when a feature flag status is toggled",fields:{flagId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},previousStatus:{type:H.String_unsecure(),isOptional:!1},newStatus:{type:H.String_unsecure(),isOptional:!1},toggledBy:{type:H.String_unsecure(),isOptional:!0},toggledAt:{type:H.DateTime(),isOptional:!1}}}),Yq=U({name:"RuleCreatedEventPayload",description:"Payload when a targeting rule is created",fields:{ruleId:{type:H.String_unsecure(),isOptional:!1},flagId:{type:H.String_unsecure(),isOptional:!1},flagKey:{type:H.String_unsecure(),isOptional:!1},attribute:{type:H.String_unsecure(),isOptional:!1},operator:{type:H.String_unsecure(),isOptional:!1},createdAt:{type:H.DateTime(),isOptional:!1}}}),Zq=U({name:"RuleDeletedEventPayload",description:"Payload when a targeting rule is deleted",fields:{ruleId:{type:H.String_unsecure(),isOptional:!1},flagId:{type:H.String_unsecure(),isOptional:!1},flagKey:{type:H.String_unsecure(),isOptional:!1},deletedAt:{type:H.DateTime(),isOptional:!1}}}),_q=U({name:"ExperimentCreatedEventPayload",description:"Payload when an experiment is created",fields:{experimentId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},name:{type:H.String_unsecure(),isOptional:!1},flagId:{type:H.String_unsecure(),isOptional:!1},variants:{type:H.JSON(),isOptional:!1},createdBy:{type:H.String_unsecure(),isOptional:!0},createdAt:{type:H.DateTime(),isOptional:!1}}}),$q=U({name:"ExperimentStartedEventPayload",description:"Payload when an experiment starts",fields:{experimentId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},flagId:{type:H.String_unsecure(),isOptional:!1},variants:{type:H.JSON(),isOptional:!1},audiencePercentage:{type:H.Int_unsecure(),isOptional:!1},startedBy:{type:H.String_unsecure(),isOptional:!0},startedAt:{type:H.DateTime(),isOptional:!1}}}),Gq=U({name:"ExperimentStoppedEventPayload",description:"Payload when an experiment stops",fields:{experimentId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},reason:{type:H.String_unsecure(),isOptional:!1},winningVariant:{type:H.String_unsecure(),isOptional:!0},stoppedBy:{type:H.String_unsecure(),isOptional:!0},stoppedAt:{type:H.DateTime(),isOptional:!1}}}),Lq=U({name:"FlagEvaluatedEventPayload",description:"Payload when a flag is evaluated (for analytics)",fields:{flagId:{type:H.String_unsecure(),isOptional:!1},flagKey:{type:H.String_unsecure(),isOptional:!1},subjectType:{type:H.String_unsecure(),isOptional:!1},subjectId:{type:H.String_unsecure(),isOptional:!1},result:{type:H.Boolean(),isOptional:!1},variant:{type:H.String_unsecure(),isOptional:!0},reason:{type:H.String_unsecure(),isOptional:!1},evaluatedAt:{type:H.DateTime(),isOptional:!1}}}),Uq=U({name:"VariantAssignedEventPayload",description:"Payload when a subject is assigned to an experiment variant",fields:{experimentId:{type:H.String_unsecure(),isOptional:!1},experimentKey:{type:H.String_unsecure(),isOptional:!1},subjectType:{type:H.String_unsecure(),isOptional:!1},subjectId:{type:H.String_unsecure(),isOptional:!1},variant:{type:H.String_unsecure(),isOptional:!1},bucket:{type:H.Int_unsecure(),isOptional:!1},assignedAt:{type:H.DateTime(),isOptional:!1}}}),Aq=L({meta:{key:"flag.created",version:"1.0.0",description:"A feature flag has been created.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","create"]},payload:Hq}),Qq=L({meta:{key:"flag.updated",version:"1.0.0",description:"A feature flag has been updated.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","update"]},payload:Jq}),Bq=L({meta:{key:"flag.deleted",version:"1.0.0",description:"A feature flag has been deleted.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","delete"]},payload:Kq}),Dq=L({meta:{key:"flag.toggled",version:"1.0.0",description:"A feature flag status has been toggled.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","toggle"]},payload:Xq}),Pq=L({meta:{key:"flag.rule_created",version:"1.0.0",description:"A targeting rule has been created.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","rule","create"]},payload:Yq}),Nq=L({meta:{key:"flag.rule_deleted",version:"1.0.0",description:"A targeting rule has been deleted.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","rule","delete"]},payload:Zq}),Wq=L({meta:{key:"experiment.created",version:"1.0.0",description:"An experiment has been created.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","experiment","create"]},payload:_q}),kq=L({meta:{key:"experiment.started",version:"1.0.0",description:"An experiment has started.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","experiment","start"]},payload:$q}),Cq=L({meta:{key:"experiment.stopped",version:"1.0.0",description:"An experiment has stopped.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","experiment","stop"]},payload:Gq}),Oq=L({meta:{key:"flag.evaluated",version:"1.0.0",description:"A feature flag has been evaluated.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","evaluate"]},payload:Lq}),Vq=L({meta:{key:"experiment.variant_assigned",version:"1.0.0",description:"A subject has been assigned to an experiment variant.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","experiment","variant"]},payload:Uq}),lq={FlagCreatedEvent:Aq,FlagUpdatedEvent:Qq,FlagDeletedEvent:Bq,FlagToggledEvent:Dq,RuleCreatedEvent:Pq,RuleDeletedEvent:Nq,ExperimentCreatedEvent:Wq,ExperimentStartedEvent:kq,ExperimentStoppedEvent:Cq,FlagEvaluatedEvent:Oq,VariantAssignedEvent:Vq};import{defineFeature as jq}from"@contractspec/lib.contracts-spec/features";var qw=jq({meta:{key:"libs.feature-flags",version:"1.0.0",title:"Feature Flags",description:"Feature flags and experiments module for ContractSpec applications",domain:"feature-flags",owners:["@contractspec-core"],tags:["package","libs","feature-flags"],stability:"experimental"},operations:[{key:"flag.create",version:"1.0.0"},{key:"flag.update",version:"1.0.0"},{key:"flag.delete",version:"1.0.0"},{key:"flag.toggle",version:"1.0.0"},{key:"flag.get",version:"1.0.0"},{key:"flag.list",version:"1.0.0"},{key:"flag.evaluate",version:"1.0.0"},{key:"flag.rule.create",version:"1.0.0"},{key:"flag.rule.delete",version:"1.0.0"},{key:"experiment.create",version:"1.0.0"},{key:"experiment.start",version:"1.0.0"},{key:"experiment.stop",version:"1.0.0"},{key:"experiment.get",version:"1.0.0"}],events:[{key:"flag.created",version:"1.0.0"},{key:"flag.updated",version:"1.0.0"},{key:"flag.deleted",version:"1.0.0"},{key:"flag.toggled",version:"1.0.0"},{key:"flag.rule_created",version:"1.0.0"},{key:"flag.rule_deleted",version:"1.0.0"},{key:"experiment.created",version:"1.0.0"},{key:"experiment.started",version:"1.0.0"},{key:"experiment.stopped",version:"1.0.0"},{key:"flag.evaluated",version:"1.0.0"},{key:"experiment.variant_assigned",version:"1.0.0"}]});export{k as hashToBucket,C as getSubjectId,cq as featureFlagsSchemaContribution,a as featureFlagEntities,e as evaluateRuleCondition,Vq as VariantAssignedEvent,vq as UpdateFlagContract,bq as ToggleFlagContract,I as TargetingRuleModel,Sq as StopExperimentContract,yq as StartExperimentContract,F as RuleOperatorEnum,Nq as RuleDeletedEvent,Pq as RuleCreatedEvent,sq as ListFlagsContract,zq as InMemoryFlagRepository,gq as GetFlagContract,Eq as GetExperimentContract,Qq as FlagUpdatedEvent,Dq as FlagToggledEvent,u as FlagTargetingRuleEntity,R as FlagStatusEnum,wq as FlagEvaluator,l as FlagEvaluationEntity,Oq as FlagEvaluatedEvent,Bq as FlagDeletedEvent,Aq as FlagCreatedEvent,qw as FeatureFlagsFeature,D as FeatureFlagModel,lq as FeatureFlagEvents,r as FeatureFlagEntity,Cq as ExperimentStoppedEvent,M as ExperimentStatusEnum,kq as ExperimentStartedEvent,W as ExperimentModel,t as ExperimentEntity,Wq as ExperimentCreatedEvent,n as ExperimentAssignmentEntity,v as EvaluationResultModel,xq as EvaluateFlagContract,fq as DeleteRuleContract,hq as DeleteFlagContract,oq as CreateRuleContract,Iq as CreateFlagContract,Tq as CreateExperimentContract};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{defineFeature as g}from"@contractspec/lib.contracts-spec";var j=g({meta:{key:"feature-flags",version:"1.0.0",title:"Feature Flags",description:"Feature
|
|
2
|
+
import{defineFeature as g}from"@contractspec/lib.contracts-spec/features";var j=g({meta:{key:"libs.feature-flags",version:"1.0.0",title:"Feature Flags",description:"Feature flags and experiments module for ContractSpec applications",domain:"feature-flags",owners:["@contractspec-core"],tags:["package","libs","feature-flags"],stability:"experimental"},operations:[{key:"flag.create",version:"1.0.0"},{key:"flag.update",version:"1.0.0"},{key:"flag.delete",version:"1.0.0"},{key:"flag.toggle",version:"1.0.0"},{key:"flag.get",version:"1.0.0"},{key:"flag.list",version:"1.0.0"},{key:"flag.evaluate",version:"1.0.0"},{key:"flag.rule.create",version:"1.0.0"},{key:"flag.rule.delete",version:"1.0.0"},{key:"experiment.create",version:"1.0.0"},{key:"experiment.start",version:"1.0.0"},{key:"experiment.stop",version:"1.0.0"},{key:"experiment.get",version:"1.0.0"}],events:[{key:"flag.created",version:"1.0.0"},{key:"flag.updated",version:"1.0.0"},{key:"flag.deleted",version:"1.0.0"},{key:"flag.toggled",version:"1.0.0"},{key:"flag.rule_created",version:"1.0.0"},{key:"flag.rule_deleted",version:"1.0.0"},{key:"experiment.created",version:"1.0.0"},{key:"experiment.started",version:"1.0.0"},{key:"experiment.stopped",version:"1.0.0"},{key:"flag.evaluated",version:"1.0.0"},{key:"experiment.variant_assigned",version:"1.0.0"}]});export{j as FeatureFlagsFeature};
|
package/dist/index.js
CHANGED
|
@@ -55,4 +55,4 @@ ${"```"},
|
|
|
55
55
|
- Ensure experiments\u2019 variant percentages sum to 100; default flag status to OFF.
|
|
56
56
|
- Use org-scoped flags for multi-tenant isolation.
|
|
57
57
|
- Log evaluations only when needed to control volume; prefer sampling for noisy paths.
|
|
58
|
-
`}];i(c);import{defineEntity as P,defineEntityEnum as V,field as z,index as $}from"@contractspec/lib.schema";var R=V({name:"FlagStatus",values:["OFF","ON","GRADUAL"],schema:"lssm_feature_flags",description:"Status of a feature flag."}),F=V({name:"RuleOperator",values:["EQ","NEQ","IN","NIN","CONTAINS","NOT_CONTAINS","GT","GTE","LT","LTE","PERCENTAGE"],schema:"lssm_feature_flags",description:"Operator for targeting rule conditions."}),M=V({name:"ExperimentStatus",values:["DRAFT","RUNNING","PAUSED","COMPLETED","CANCELLED"],schema:"lssm_feature_flags",description:"Status of an experiment."}),r=P({name:"FeatureFlag",description:"A feature flag for controlling feature availability.",schema:"lssm_feature_flags",map:"feature_flag",fields:{id:z.id({description:"Unique flag identifier"}),key:z.string({isUnique:!0,description:"Flag key (e.g., new_dashboard)"}),name:z.string({description:"Human-readable name"}),description:z.string({isOptional:!0,description:"Description of the flag"}),status:z.enum("FlagStatus",{default:"OFF",description:"Flag status"}),defaultValue:z.boolean({default:!1,description:"Default value when no rules match"}),variants:z.json({isOptional:!0,description:"Variant definitions for multivariate flags"}),orgId:z.string({isOptional:!0,description:"Organization scope (null = global)"}),tags:z.json({isOptional:!0,description:"Tags for categorization"}),metadata:z.json({isOptional:!0,description:"Additional metadata"}),createdAt:z.createdAt(),updatedAt:z.updatedAt(),targetingRules:z.hasMany("FlagTargetingRule"),experiments:z.hasMany("Experiment"),evaluations:z.hasMany("FlagEvaluation")},indexes:[$.on(["orgId","key"]),$.on(["status"])],enums:[R]}),u=P({name:"FlagTargetingRule",description:"A targeting rule for conditional flag evaluation.",schema:"lssm_feature_flags",map:"flag_targeting_rule",fields:{id:z.id({description:"Unique rule identifier"}),flagId:z.foreignKey({description:"Parent feature flag"}),name:z.string({isOptional:!0,description:"Rule name for debugging"}),priority:z.int({default:0,description:"Rule priority (lower = higher priority)"}),enabled:z.boolean({default:!0,description:"Whether rule is active"}),attribute:z.string({description:"Target attribute (userId, orgId, plan, segment, etc.)"}),operator:z.enum("RuleOperator",{description:"Comparison operator"}),value:z.json({description:"Target value(s)"}),rolloutPercentage:z.int({isOptional:!0,description:"Percentage for gradual rollout (0-100)"}),serveValue:z.boolean({isOptional:!0,description:"Boolean value to serve"}),serveVariant:z.string({isOptional:!0,description:"Variant key to serve (for multivariate)"}),createdAt:z.createdAt(),updatedAt:z.updatedAt(),flag:z.belongsTo("FeatureFlag",["flagId"],["id"],{onDelete:"Cascade"})},indexes:[$.on(["flagId","priority"]),$.on(["attribute"])],enums:[F]}),t=P({name:"Experiment",description:"An A/B test experiment.",schema:"lssm_feature_flags",map:"experiment",fields:{id:z.id({description:"Unique experiment identifier"}),key:z.string({isUnique:!0,description:"Experiment key"}),name:z.string({description:"Human-readable name"}),description:z.string({isOptional:!0,description:"Experiment description"}),hypothesis:z.string({isOptional:!0,description:"Experiment hypothesis"}),flagId:z.foreignKey({description:"Associated feature flag"}),status:z.enum("ExperimentStatus",{default:"DRAFT",description:"Experiment status"}),variants:z.json({description:"Variant definitions with split ratios"}),metrics:z.json({isOptional:!0,description:"Metrics to track"}),audiencePercentage:z.int({default:100,description:"Percentage of audience to include"}),audienceFilter:z.json({isOptional:!0,description:"Audience filter criteria"}),scheduledStartAt:z.dateTime({isOptional:!0,description:"Scheduled start time"}),scheduledEndAt:z.dateTime({isOptional:!0,description:"Scheduled end time"}),startedAt:z.dateTime({isOptional:!0,description:"Actual start time"}),endedAt:z.dateTime({isOptional:!0,description:"Actual end time"}),winningVariant:z.string({isOptional:!0,description:"Declared winning variant"}),results:z.json({isOptional:!0,description:"Experiment results summary"}),orgId:z.string({isOptional:!0,description:"Organization scope"}),createdAt:z.createdAt(),updatedAt:z.updatedAt(),flag:z.belongsTo("FeatureFlag",["flagId"],["id"],{onDelete:"Cascade"}),assignments:z.hasMany("ExperimentAssignment")},indexes:[$.on(["status"]),$.on(["orgId","status"]),$.on(["flagId"])],enums:[M]}),n=P({name:"ExperimentAssignment",description:"Tracks experiment variant assignments.",schema:"lssm_feature_flags",map:"experiment_assignment",fields:{id:z.id({description:"Unique assignment identifier"}),experimentId:z.foreignKey({description:"Parent experiment"}),subjectType:z.string({description:"Subject type (user, org, session)"}),subjectId:z.string({description:"Subject identifier"}),variant:z.string({description:"Assigned variant key"}),bucket:z.int({description:"Hash bucket (0-99)"}),context:z.json({isOptional:!0,description:"Context at assignment time"}),assignedAt:z.dateTime({description:"Assignment timestamp"}),experiment:z.belongsTo("Experiment",["experimentId"],["id"],{onDelete:"Cascade"})},indexes:[$.unique(["experimentId","subjectType","subjectId"],{name:"experiment_assignment_unique"}),$.on(["subjectType","subjectId"])]}),l=P({name:"FlagEvaluation",description:"Log of flag evaluations for debugging and analytics.",schema:"lssm_feature_flags",map:"flag_evaluation",fields:{id:z.id({description:"Unique evaluation identifier"}),flagId:z.foreignKey({description:"Evaluated flag"}),flagKey:z.string({description:"Flag key (denormalized for queries)"}),subjectType:z.string({description:"Subject type (user, org, anonymous)"}),subjectId:z.string({description:"Subject identifier"}),result:z.boolean({description:"Evaluation result"}),variant:z.string({isOptional:!0,description:"Served variant (for multivariate)"}),matchedRuleId:z.string({isOptional:!0,description:"Rule that matched (if any)"}),reason:z.string({description:"Evaluation reason (default, rule, experiment, etc.)"}),context:z.json({isOptional:!0,description:"Evaluation context"}),evaluatedAt:z.dateTime({description:"Evaluation timestamp"}),flag:z.belongsTo("FeatureFlag",["flagId"],["id"],{onDelete:"Cascade"})},indexes:[$.on(["flagKey","evaluatedAt"]),$.on(["subjectType","subjectId","evaluatedAt"]),$.on(["flagId","evaluatedAt"])]}),a=[r,u,t,n,l],cq={moduleId:"@contractspec/lib.feature-flags",entities:a,enums:[R,F,M]};function k(w,K=""){let J=`${K}:${w}`,X=0;for(let G=0;G<J.length;G++){let A=J.charCodeAt(G);X=(X<<5)-X+A,X=X&X}return Math.abs(X%100)}function C(w){return w.userId||w.sessionId||w.orgId||"anonymous"}function e(w,K){let J=qq(w.attribute,K);switch(w.operator){case"EQ":return J===w.value;case"NEQ":return J!==w.value;case"IN":if(!Array.isArray(w.value))return!1;return w.value.includes(J);case"NIN":if(!Array.isArray(w.value))return!0;return!w.value.includes(J);case"CONTAINS":if(typeof J!=="string"||typeof w.value!=="string")return!1;return J.includes(w.value);case"NOT_CONTAINS":if(typeof J!=="string"||typeof w.value!=="string")return!0;return!J.includes(w.value);case"GT":if(typeof J!=="number"||typeof w.value!=="number")return!1;return J>w.value;case"GTE":if(typeof J!=="number"||typeof w.value!=="number")return!1;return J>=w.value;case"LT":if(typeof J!=="number"||typeof w.value!=="number")return!1;return J<w.value;case"LTE":if(typeof J!=="number"||typeof w.value!=="number")return!1;return J<=w.value;case"PERCENTAGE":return k(C(K),w.attribute)<(typeof w.value==="number"?w.value:0);default:return!1}}function qq(w,K){switch(w){case"userId":return K.userId;case"orgId":return K.orgId;case"plan":return K.plan;case"segment":return K.segment;case"sessionId":return K.sessionId;default:return K.attributes?.[w]}}class wq{repository;logger;logEvaluations;constructor(w){this.repository=w.repository,this.logger=w.logger,this.logEvaluations=w.logEvaluations??!1}async evaluate(w,K){let J=K.orgId,X=await this.repository.getFlag(w,J);if(!X)return this.makeResult(!1,"FLAG_NOT_FOUND");if(X.status==="OFF")return this.logAndReturn(X,K,this.makeResult(!1,"FLAG_OFF"));if(X.status==="ON")return this.logAndReturn(X,K,this.makeResult(!0,"FLAG_ON"));let A=[...await this.repository.getRules(X.id)].filter((Z)=>Z.enabled).sort((Z,O)=>Z.priority-O.priority);for(let Z of A)if(e(Z,K)){if(Z.rolloutPercentage!==void 0&&Z.rolloutPercentage!==null){if(k(C(K),X.key)>=Z.rolloutPercentage)continue}let O=Z.serveValue??!0;return this.logAndReturn(X,K,this.makeResult(O,"RULE_MATCH",Z.serveVariant,Z.id))}let B=await this.repository.getActiveExperiment(X.id);if(B&&B.status==="RUNNING"){let Z=await this.evaluateExperiment(B,K);if(Z)return this.logAndReturn(X,K,Z)}return this.logAndReturn(X,K,this.makeResult(X.defaultValue,"DEFAULT_VALUE"))}async evaluateExperiment(w,K){let J=C(K),X=K.userId?"user":K.orgId?"org":"session";if(k(J,`${w.key}:audience`)>=w.audiencePercentage)return null;let A=await this.repository.getExperimentAssignment(w.id,X,J);if(!A){let Z=k(J,w.key);A=this.assignVariant(w.variants,Z),await this.repository.saveExperimentAssignment(w.id,X,J,A,Z)}let B=A!=="control";return this.makeResult(B,"EXPERIMENT_VARIANT",A,void 0,w.id)}assignVariant(w,K){let J=0;for(let X of w)if(J+=X.percentage,K<J)return X.key;return w[w.length-1]?.key??"control"}makeResult(w,K,J,X,G){return{enabled:w,variant:J,reason:K,ruleId:X,experimentId:G}}logAndReturn(w,K,J){if(this.logEvaluations&&this.logger){let X=C(K),G=K.userId?"user":K.orgId?"org":"session";this.logger.log({flagId:w.id,flagKey:w.key,subjectType:G,subjectId:X,result:J.enabled,variant:J.variant,reason:J.reason,ruleId:J.ruleId,experimentId:J.experimentId,context:K})}return J}}class zq{flags=new Map;rules=new Map;experiments=new Map;assignments=new Map;addFlag(w){this.flags.set(w.key,w)}addRule(w,K){let J=this.rules.get(w)||[];J.push(K),this.rules.set(w,J)}addExperiment(w,K){this.experiments.set(K,w)}async getFlag(w){return this.flags.get(w)||null}async getRules(w){return this.rules.get(w)||[]}async getActiveExperiment(w){return this.experiments.get(w)||null}async getExperimentAssignment(w,K,J){let X=`${w}:${K}:${J}`;return this.assignments.get(X)||null}async saveExperimentAssignment(w,K,J,X){let G=`${w}:${K}:${J}`;this.assignments.set(G,X)}clear(){this.flags.clear(),this.rules.clear(),this.experiments.clear(),this.assignments.clear()}}import{defineEvent as L}from"@contractspec/lib.contracts-spec";import{defineSchemaModel as U,ScalarTypeEnum as H}from"@contractspec/lib.schema";var Hq=U({name:"FlagCreatedEventPayload",description:"Payload when a feature flag is created",fields:{flagId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},name:{type:H.String_unsecure(),isOptional:!1},status:{type:H.String_unsecure(),isOptional:!1},orgId:{type:H.String_unsecure(),isOptional:!0},createdBy:{type:H.String_unsecure(),isOptional:!0},createdAt:{type:H.DateTime(),isOptional:!1}}}),Jq=U({name:"FlagUpdatedEventPayload",description:"Payload when a feature flag is updated",fields:{flagId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},changes:{type:H.JSON(),isOptional:!1},updatedBy:{type:H.String_unsecure(),isOptional:!0},updatedAt:{type:H.DateTime(),isOptional:!1}}}),Kq=U({name:"FlagDeletedEventPayload",description:"Payload when a feature flag is deleted",fields:{flagId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},deletedBy:{type:H.String_unsecure(),isOptional:!0},deletedAt:{type:H.DateTime(),isOptional:!1}}}),Xq=U({name:"FlagToggledEventPayload",description:"Payload when a feature flag status is toggled",fields:{flagId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},previousStatus:{type:H.String_unsecure(),isOptional:!1},newStatus:{type:H.String_unsecure(),isOptional:!1},toggledBy:{type:H.String_unsecure(),isOptional:!0},toggledAt:{type:H.DateTime(),isOptional:!1}}}),Yq=U({name:"RuleCreatedEventPayload",description:"Payload when a targeting rule is created",fields:{ruleId:{type:H.String_unsecure(),isOptional:!1},flagId:{type:H.String_unsecure(),isOptional:!1},flagKey:{type:H.String_unsecure(),isOptional:!1},attribute:{type:H.String_unsecure(),isOptional:!1},operator:{type:H.String_unsecure(),isOptional:!1},createdAt:{type:H.DateTime(),isOptional:!1}}}),Zq=U({name:"RuleDeletedEventPayload",description:"Payload when a targeting rule is deleted",fields:{ruleId:{type:H.String_unsecure(),isOptional:!1},flagId:{type:H.String_unsecure(),isOptional:!1},flagKey:{type:H.String_unsecure(),isOptional:!1},deletedAt:{type:H.DateTime(),isOptional:!1}}}),_q=U({name:"ExperimentCreatedEventPayload",description:"Payload when an experiment is created",fields:{experimentId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},name:{type:H.String_unsecure(),isOptional:!1},flagId:{type:H.String_unsecure(),isOptional:!1},variants:{type:H.JSON(),isOptional:!1},createdBy:{type:H.String_unsecure(),isOptional:!0},createdAt:{type:H.DateTime(),isOptional:!1}}}),$q=U({name:"ExperimentStartedEventPayload",description:"Payload when an experiment starts",fields:{experimentId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},flagId:{type:H.String_unsecure(),isOptional:!1},variants:{type:H.JSON(),isOptional:!1},audiencePercentage:{type:H.Int_unsecure(),isOptional:!1},startedBy:{type:H.String_unsecure(),isOptional:!0},startedAt:{type:H.DateTime(),isOptional:!1}}}),Gq=U({name:"ExperimentStoppedEventPayload",description:"Payload when an experiment stops",fields:{experimentId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},reason:{type:H.String_unsecure(),isOptional:!1},winningVariant:{type:H.String_unsecure(),isOptional:!0},stoppedBy:{type:H.String_unsecure(),isOptional:!0},stoppedAt:{type:H.DateTime(),isOptional:!1}}}),Lq=U({name:"FlagEvaluatedEventPayload",description:"Payload when a flag is evaluated (for analytics)",fields:{flagId:{type:H.String_unsecure(),isOptional:!1},flagKey:{type:H.String_unsecure(),isOptional:!1},subjectType:{type:H.String_unsecure(),isOptional:!1},subjectId:{type:H.String_unsecure(),isOptional:!1},result:{type:H.Boolean(),isOptional:!1},variant:{type:H.String_unsecure(),isOptional:!0},reason:{type:H.String_unsecure(),isOptional:!1},evaluatedAt:{type:H.DateTime(),isOptional:!1}}}),Uq=U({name:"VariantAssignedEventPayload",description:"Payload when a subject is assigned to an experiment variant",fields:{experimentId:{type:H.String_unsecure(),isOptional:!1},experimentKey:{type:H.String_unsecure(),isOptional:!1},subjectType:{type:H.String_unsecure(),isOptional:!1},subjectId:{type:H.String_unsecure(),isOptional:!1},variant:{type:H.String_unsecure(),isOptional:!1},bucket:{type:H.Int_unsecure(),isOptional:!1},assignedAt:{type:H.DateTime(),isOptional:!1}}}),Aq=L({meta:{key:"flag.created",version:"1.0.0",description:"A feature flag has been created.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","create"]},payload:Hq}),Qq=L({meta:{key:"flag.updated",version:"1.0.0",description:"A feature flag has been updated.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","update"]},payload:Jq}),Bq=L({meta:{key:"flag.deleted",version:"1.0.0",description:"A feature flag has been deleted.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","delete"]},payload:Kq}),Dq=L({meta:{key:"flag.toggled",version:"1.0.0",description:"A feature flag status has been toggled.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","toggle"]},payload:Xq}),Pq=L({meta:{key:"flag.rule_created",version:"1.0.0",description:"A targeting rule has been created.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","rule","create"]},payload:Yq}),Nq=L({meta:{key:"flag.rule_deleted",version:"1.0.0",description:"A targeting rule has been deleted.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","rule","delete"]},payload:Zq}),Wq=L({meta:{key:"experiment.created",version:"1.0.0",description:"An experiment has been created.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","experiment","create"]},payload:_q}),kq=L({meta:{key:"experiment.started",version:"1.0.0",description:"An experiment has started.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","experiment","start"]},payload:$q}),Cq=L({meta:{key:"experiment.stopped",version:"1.0.0",description:"An experiment has stopped.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","experiment","stop"]},payload:Gq}),Oq=L({meta:{key:"flag.evaluated",version:"1.0.0",description:"A feature flag has been evaluated.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","evaluate"]},payload:Lq}),Vq=L({meta:{key:"experiment.variant_assigned",version:"1.0.0",description:"A subject has been assigned to an experiment variant.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","experiment","variant"]},payload:Uq}),lq={FlagCreatedEvent:Aq,FlagUpdatedEvent:Qq,FlagDeletedEvent:Bq,FlagToggledEvent:Dq,RuleCreatedEvent:Pq,RuleDeletedEvent:Nq,ExperimentCreatedEvent:Wq,ExperimentStartedEvent:kq,ExperimentStoppedEvent:Cq,FlagEvaluatedEvent:Oq,VariantAssignedEvent:Vq};import{defineFeature as jq}from"@contractspec/lib.contracts-spec";var qw=jq({meta:{key:"feature-flags",version:"1.0.0",title:"Feature Flags",description:"Feature flag management with targeting rules and A/B experiments",domain:"platform",owners:["@platform.feature-flags"],tags:["feature-flags","experiments","targeting"],stability:"stable"},operations:[{key:"flag.create",version:"1.0.0"},{key:"flag.update",version:"1.0.0"},{key:"flag.delete",version:"1.0.0"},{key:"flag.toggle",version:"1.0.0"},{key:"flag.get",version:"1.0.0"},{key:"flag.list",version:"1.0.0"},{key:"flag.evaluate",version:"1.0.0"},{key:"flag.rule.create",version:"1.0.0"},{key:"flag.rule.delete",version:"1.0.0"},{key:"experiment.create",version:"1.0.0"},{key:"experiment.start",version:"1.0.0"},{key:"experiment.stop",version:"1.0.0"},{key:"experiment.get",version:"1.0.0"}],events:[{key:"flag.created",version:"1.0.0"},{key:"flag.updated",version:"1.0.0"},{key:"flag.deleted",version:"1.0.0"},{key:"flag.toggled",version:"1.0.0"},{key:"flag.evaluated",version:"1.0.0"},{key:"flag.rule_created",version:"1.0.0"},{key:"flag.rule_deleted",version:"1.0.0"},{key:"experiment.created",version:"1.0.0"},{key:"experiment.started",version:"1.0.0"},{key:"experiment.stopped",version:"1.0.0"},{key:"experiment.variant_assigned",version:"1.0.0"}],presentations:[],opToPresentation:[],presentationsTargets:[],capabilities:{provides:[{key:"feature-flag",version:"1.0.0"},{key:"experiments",version:"1.0.0"}],requires:[{key:"identity",version:"1.0.0"}]}});export{k as hashToBucket,C as getSubjectId,cq as featureFlagsSchemaContribution,a as featureFlagEntities,e as evaluateRuleCondition,Vq as VariantAssignedEvent,vq as UpdateFlagContract,bq as ToggleFlagContract,I as TargetingRuleModel,Sq as StopExperimentContract,yq as StartExperimentContract,F as RuleOperatorEnum,Nq as RuleDeletedEvent,Pq as RuleCreatedEvent,sq as ListFlagsContract,zq as InMemoryFlagRepository,gq as GetFlagContract,Eq as GetExperimentContract,Qq as FlagUpdatedEvent,Dq as FlagToggledEvent,u as FlagTargetingRuleEntity,R as FlagStatusEnum,wq as FlagEvaluator,l as FlagEvaluationEntity,Oq as FlagEvaluatedEvent,Bq as FlagDeletedEvent,Aq as FlagCreatedEvent,qw as FeatureFlagsFeature,D as FeatureFlagModel,lq as FeatureFlagEvents,r as FeatureFlagEntity,Cq as ExperimentStoppedEvent,M as ExperimentStatusEnum,kq as ExperimentStartedEvent,W as ExperimentModel,t as ExperimentEntity,Wq as ExperimentCreatedEvent,n as ExperimentAssignmentEntity,v as EvaluationResultModel,xq as EvaluateFlagContract,fq as DeleteRuleContract,hq as DeleteFlagContract,oq as CreateRuleContract,Iq as CreateFlagContract,Tq as CreateExperimentContract};
|
|
58
|
+
`}];i(c);import{defineEntity as P,defineEntityEnum as V,field as z,index as $}from"@contractspec/lib.schema";var R=V({name:"FlagStatus",values:["OFF","ON","GRADUAL"],schema:"lssm_feature_flags",description:"Status of a feature flag."}),F=V({name:"RuleOperator",values:["EQ","NEQ","IN","NIN","CONTAINS","NOT_CONTAINS","GT","GTE","LT","LTE","PERCENTAGE"],schema:"lssm_feature_flags",description:"Operator for targeting rule conditions."}),M=V({name:"ExperimentStatus",values:["DRAFT","RUNNING","PAUSED","COMPLETED","CANCELLED"],schema:"lssm_feature_flags",description:"Status of an experiment."}),r=P({name:"FeatureFlag",description:"A feature flag for controlling feature availability.",schema:"lssm_feature_flags",map:"feature_flag",fields:{id:z.id({description:"Unique flag identifier"}),key:z.string({isUnique:!0,description:"Flag key (e.g., new_dashboard)"}),name:z.string({description:"Human-readable name"}),description:z.string({isOptional:!0,description:"Description of the flag"}),status:z.enum("FlagStatus",{default:"OFF",description:"Flag status"}),defaultValue:z.boolean({default:!1,description:"Default value when no rules match"}),variants:z.json({isOptional:!0,description:"Variant definitions for multivariate flags"}),orgId:z.string({isOptional:!0,description:"Organization scope (null = global)"}),tags:z.json({isOptional:!0,description:"Tags for categorization"}),metadata:z.json({isOptional:!0,description:"Additional metadata"}),createdAt:z.createdAt(),updatedAt:z.updatedAt(),targetingRules:z.hasMany("FlagTargetingRule"),experiments:z.hasMany("Experiment"),evaluations:z.hasMany("FlagEvaluation")},indexes:[$.on(["orgId","key"]),$.on(["status"])],enums:[R]}),u=P({name:"FlagTargetingRule",description:"A targeting rule for conditional flag evaluation.",schema:"lssm_feature_flags",map:"flag_targeting_rule",fields:{id:z.id({description:"Unique rule identifier"}),flagId:z.foreignKey({description:"Parent feature flag"}),name:z.string({isOptional:!0,description:"Rule name for debugging"}),priority:z.int({default:0,description:"Rule priority (lower = higher priority)"}),enabled:z.boolean({default:!0,description:"Whether rule is active"}),attribute:z.string({description:"Target attribute (userId, orgId, plan, segment, etc.)"}),operator:z.enum("RuleOperator",{description:"Comparison operator"}),value:z.json({description:"Target value(s)"}),rolloutPercentage:z.int({isOptional:!0,description:"Percentage for gradual rollout (0-100)"}),serveValue:z.boolean({isOptional:!0,description:"Boolean value to serve"}),serveVariant:z.string({isOptional:!0,description:"Variant key to serve (for multivariate)"}),createdAt:z.createdAt(),updatedAt:z.updatedAt(),flag:z.belongsTo("FeatureFlag",["flagId"],["id"],{onDelete:"Cascade"})},indexes:[$.on(["flagId","priority"]),$.on(["attribute"])],enums:[F]}),t=P({name:"Experiment",description:"An A/B test experiment.",schema:"lssm_feature_flags",map:"experiment",fields:{id:z.id({description:"Unique experiment identifier"}),key:z.string({isUnique:!0,description:"Experiment key"}),name:z.string({description:"Human-readable name"}),description:z.string({isOptional:!0,description:"Experiment description"}),hypothesis:z.string({isOptional:!0,description:"Experiment hypothesis"}),flagId:z.foreignKey({description:"Associated feature flag"}),status:z.enum("ExperimentStatus",{default:"DRAFT",description:"Experiment status"}),variants:z.json({description:"Variant definitions with split ratios"}),metrics:z.json({isOptional:!0,description:"Metrics to track"}),audiencePercentage:z.int({default:100,description:"Percentage of audience to include"}),audienceFilter:z.json({isOptional:!0,description:"Audience filter criteria"}),scheduledStartAt:z.dateTime({isOptional:!0,description:"Scheduled start time"}),scheduledEndAt:z.dateTime({isOptional:!0,description:"Scheduled end time"}),startedAt:z.dateTime({isOptional:!0,description:"Actual start time"}),endedAt:z.dateTime({isOptional:!0,description:"Actual end time"}),winningVariant:z.string({isOptional:!0,description:"Declared winning variant"}),results:z.json({isOptional:!0,description:"Experiment results summary"}),orgId:z.string({isOptional:!0,description:"Organization scope"}),createdAt:z.createdAt(),updatedAt:z.updatedAt(),flag:z.belongsTo("FeatureFlag",["flagId"],["id"],{onDelete:"Cascade"}),assignments:z.hasMany("ExperimentAssignment")},indexes:[$.on(["status"]),$.on(["orgId","status"]),$.on(["flagId"])],enums:[M]}),n=P({name:"ExperimentAssignment",description:"Tracks experiment variant assignments.",schema:"lssm_feature_flags",map:"experiment_assignment",fields:{id:z.id({description:"Unique assignment identifier"}),experimentId:z.foreignKey({description:"Parent experiment"}),subjectType:z.string({description:"Subject type (user, org, session)"}),subjectId:z.string({description:"Subject identifier"}),variant:z.string({description:"Assigned variant key"}),bucket:z.int({description:"Hash bucket (0-99)"}),context:z.json({isOptional:!0,description:"Context at assignment time"}),assignedAt:z.dateTime({description:"Assignment timestamp"}),experiment:z.belongsTo("Experiment",["experimentId"],["id"],{onDelete:"Cascade"})},indexes:[$.unique(["experimentId","subjectType","subjectId"],{name:"experiment_assignment_unique"}),$.on(["subjectType","subjectId"])]}),l=P({name:"FlagEvaluation",description:"Log of flag evaluations for debugging and analytics.",schema:"lssm_feature_flags",map:"flag_evaluation",fields:{id:z.id({description:"Unique evaluation identifier"}),flagId:z.foreignKey({description:"Evaluated flag"}),flagKey:z.string({description:"Flag key (denormalized for queries)"}),subjectType:z.string({description:"Subject type (user, org, anonymous)"}),subjectId:z.string({description:"Subject identifier"}),result:z.boolean({description:"Evaluation result"}),variant:z.string({isOptional:!0,description:"Served variant (for multivariate)"}),matchedRuleId:z.string({isOptional:!0,description:"Rule that matched (if any)"}),reason:z.string({description:"Evaluation reason (default, rule, experiment, etc.)"}),context:z.json({isOptional:!0,description:"Evaluation context"}),evaluatedAt:z.dateTime({description:"Evaluation timestamp"}),flag:z.belongsTo("FeatureFlag",["flagId"],["id"],{onDelete:"Cascade"})},indexes:[$.on(["flagKey","evaluatedAt"]),$.on(["subjectType","subjectId","evaluatedAt"]),$.on(["flagId","evaluatedAt"])]}),a=[r,u,t,n,l],cq={moduleId:"@contractspec/lib.feature-flags",entities:a,enums:[R,F,M]};function k(w,K=""){let J=`${K}:${w}`,X=0;for(let G=0;G<J.length;G++){let A=J.charCodeAt(G);X=(X<<5)-X+A,X=X&X}return Math.abs(X%100)}function C(w){return w.userId||w.sessionId||w.orgId||"anonymous"}function e(w,K){let J=qq(w.attribute,K);switch(w.operator){case"EQ":return J===w.value;case"NEQ":return J!==w.value;case"IN":if(!Array.isArray(w.value))return!1;return w.value.includes(J);case"NIN":if(!Array.isArray(w.value))return!0;return!w.value.includes(J);case"CONTAINS":if(typeof J!=="string"||typeof w.value!=="string")return!1;return J.includes(w.value);case"NOT_CONTAINS":if(typeof J!=="string"||typeof w.value!=="string")return!0;return!J.includes(w.value);case"GT":if(typeof J!=="number"||typeof w.value!=="number")return!1;return J>w.value;case"GTE":if(typeof J!=="number"||typeof w.value!=="number")return!1;return J>=w.value;case"LT":if(typeof J!=="number"||typeof w.value!=="number")return!1;return J<w.value;case"LTE":if(typeof J!=="number"||typeof w.value!=="number")return!1;return J<=w.value;case"PERCENTAGE":return k(C(K),w.attribute)<(typeof w.value==="number"?w.value:0);default:return!1}}function qq(w,K){switch(w){case"userId":return K.userId;case"orgId":return K.orgId;case"plan":return K.plan;case"segment":return K.segment;case"sessionId":return K.sessionId;default:return K.attributes?.[w]}}class wq{repository;logger;logEvaluations;constructor(w){this.repository=w.repository,this.logger=w.logger,this.logEvaluations=w.logEvaluations??!1}async evaluate(w,K){let J=K.orgId,X=await this.repository.getFlag(w,J);if(!X)return this.makeResult(!1,"FLAG_NOT_FOUND");if(X.status==="OFF")return this.logAndReturn(X,K,this.makeResult(!1,"FLAG_OFF"));if(X.status==="ON")return this.logAndReturn(X,K,this.makeResult(!0,"FLAG_ON"));let A=[...await this.repository.getRules(X.id)].filter((Z)=>Z.enabled).sort((Z,O)=>Z.priority-O.priority);for(let Z of A)if(e(Z,K)){if(Z.rolloutPercentage!==void 0&&Z.rolloutPercentage!==null){if(k(C(K),X.key)>=Z.rolloutPercentage)continue}let O=Z.serveValue??!0;return this.logAndReturn(X,K,this.makeResult(O,"RULE_MATCH",Z.serveVariant,Z.id))}let B=await this.repository.getActiveExperiment(X.id);if(B&&B.status==="RUNNING"){let Z=await this.evaluateExperiment(B,K);if(Z)return this.logAndReturn(X,K,Z)}return this.logAndReturn(X,K,this.makeResult(X.defaultValue,"DEFAULT_VALUE"))}async evaluateExperiment(w,K){let J=C(K),X=K.userId?"user":K.orgId?"org":"session";if(k(J,`${w.key}:audience`)>=w.audiencePercentage)return null;let A=await this.repository.getExperimentAssignment(w.id,X,J);if(!A){let Z=k(J,w.key);A=this.assignVariant(w.variants,Z),await this.repository.saveExperimentAssignment(w.id,X,J,A,Z)}let B=A!=="control";return this.makeResult(B,"EXPERIMENT_VARIANT",A,void 0,w.id)}assignVariant(w,K){let J=0;for(let X of w)if(J+=X.percentage,K<J)return X.key;return w[w.length-1]?.key??"control"}makeResult(w,K,J,X,G){return{enabled:w,variant:J,reason:K,ruleId:X,experimentId:G}}logAndReturn(w,K,J){if(this.logEvaluations&&this.logger){let X=C(K),G=K.userId?"user":K.orgId?"org":"session";this.logger.log({flagId:w.id,flagKey:w.key,subjectType:G,subjectId:X,result:J.enabled,variant:J.variant,reason:J.reason,ruleId:J.ruleId,experimentId:J.experimentId,context:K})}return J}}class zq{flags=new Map;rules=new Map;experiments=new Map;assignments=new Map;addFlag(w){this.flags.set(w.key,w)}addRule(w,K){let J=this.rules.get(w)||[];J.push(K),this.rules.set(w,J)}addExperiment(w,K){this.experiments.set(K,w)}async getFlag(w){return this.flags.get(w)||null}async getRules(w){return this.rules.get(w)||[]}async getActiveExperiment(w){return this.experiments.get(w)||null}async getExperimentAssignment(w,K,J){let X=`${w}:${K}:${J}`;return this.assignments.get(X)||null}async saveExperimentAssignment(w,K,J,X){let G=`${w}:${K}:${J}`;this.assignments.set(G,X)}clear(){this.flags.clear(),this.rules.clear(),this.experiments.clear(),this.assignments.clear()}}import{defineEvent as L}from"@contractspec/lib.contracts-spec";import{defineSchemaModel as U,ScalarTypeEnum as H}from"@contractspec/lib.schema";var Hq=U({name:"FlagCreatedEventPayload",description:"Payload when a feature flag is created",fields:{flagId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},name:{type:H.String_unsecure(),isOptional:!1},status:{type:H.String_unsecure(),isOptional:!1},orgId:{type:H.String_unsecure(),isOptional:!0},createdBy:{type:H.String_unsecure(),isOptional:!0},createdAt:{type:H.DateTime(),isOptional:!1}}}),Jq=U({name:"FlagUpdatedEventPayload",description:"Payload when a feature flag is updated",fields:{flagId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},changes:{type:H.JSON(),isOptional:!1},updatedBy:{type:H.String_unsecure(),isOptional:!0},updatedAt:{type:H.DateTime(),isOptional:!1}}}),Kq=U({name:"FlagDeletedEventPayload",description:"Payload when a feature flag is deleted",fields:{flagId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},deletedBy:{type:H.String_unsecure(),isOptional:!0},deletedAt:{type:H.DateTime(),isOptional:!1}}}),Xq=U({name:"FlagToggledEventPayload",description:"Payload when a feature flag status is toggled",fields:{flagId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},previousStatus:{type:H.String_unsecure(),isOptional:!1},newStatus:{type:H.String_unsecure(),isOptional:!1},toggledBy:{type:H.String_unsecure(),isOptional:!0},toggledAt:{type:H.DateTime(),isOptional:!1}}}),Yq=U({name:"RuleCreatedEventPayload",description:"Payload when a targeting rule is created",fields:{ruleId:{type:H.String_unsecure(),isOptional:!1},flagId:{type:H.String_unsecure(),isOptional:!1},flagKey:{type:H.String_unsecure(),isOptional:!1},attribute:{type:H.String_unsecure(),isOptional:!1},operator:{type:H.String_unsecure(),isOptional:!1},createdAt:{type:H.DateTime(),isOptional:!1}}}),Zq=U({name:"RuleDeletedEventPayload",description:"Payload when a targeting rule is deleted",fields:{ruleId:{type:H.String_unsecure(),isOptional:!1},flagId:{type:H.String_unsecure(),isOptional:!1},flagKey:{type:H.String_unsecure(),isOptional:!1},deletedAt:{type:H.DateTime(),isOptional:!1}}}),_q=U({name:"ExperimentCreatedEventPayload",description:"Payload when an experiment is created",fields:{experimentId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},name:{type:H.String_unsecure(),isOptional:!1},flagId:{type:H.String_unsecure(),isOptional:!1},variants:{type:H.JSON(),isOptional:!1},createdBy:{type:H.String_unsecure(),isOptional:!0},createdAt:{type:H.DateTime(),isOptional:!1}}}),$q=U({name:"ExperimentStartedEventPayload",description:"Payload when an experiment starts",fields:{experimentId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},flagId:{type:H.String_unsecure(),isOptional:!1},variants:{type:H.JSON(),isOptional:!1},audiencePercentage:{type:H.Int_unsecure(),isOptional:!1},startedBy:{type:H.String_unsecure(),isOptional:!0},startedAt:{type:H.DateTime(),isOptional:!1}}}),Gq=U({name:"ExperimentStoppedEventPayload",description:"Payload when an experiment stops",fields:{experimentId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},reason:{type:H.String_unsecure(),isOptional:!1},winningVariant:{type:H.String_unsecure(),isOptional:!0},stoppedBy:{type:H.String_unsecure(),isOptional:!0},stoppedAt:{type:H.DateTime(),isOptional:!1}}}),Lq=U({name:"FlagEvaluatedEventPayload",description:"Payload when a flag is evaluated (for analytics)",fields:{flagId:{type:H.String_unsecure(),isOptional:!1},flagKey:{type:H.String_unsecure(),isOptional:!1},subjectType:{type:H.String_unsecure(),isOptional:!1},subjectId:{type:H.String_unsecure(),isOptional:!1},result:{type:H.Boolean(),isOptional:!1},variant:{type:H.String_unsecure(),isOptional:!0},reason:{type:H.String_unsecure(),isOptional:!1},evaluatedAt:{type:H.DateTime(),isOptional:!1}}}),Uq=U({name:"VariantAssignedEventPayload",description:"Payload when a subject is assigned to an experiment variant",fields:{experimentId:{type:H.String_unsecure(),isOptional:!1},experimentKey:{type:H.String_unsecure(),isOptional:!1},subjectType:{type:H.String_unsecure(),isOptional:!1},subjectId:{type:H.String_unsecure(),isOptional:!1},variant:{type:H.String_unsecure(),isOptional:!1},bucket:{type:H.Int_unsecure(),isOptional:!1},assignedAt:{type:H.DateTime(),isOptional:!1}}}),Aq=L({meta:{key:"flag.created",version:"1.0.0",description:"A feature flag has been created.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","create"]},payload:Hq}),Qq=L({meta:{key:"flag.updated",version:"1.0.0",description:"A feature flag has been updated.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","update"]},payload:Jq}),Bq=L({meta:{key:"flag.deleted",version:"1.0.0",description:"A feature flag has been deleted.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","delete"]},payload:Kq}),Dq=L({meta:{key:"flag.toggled",version:"1.0.0",description:"A feature flag status has been toggled.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","toggle"]},payload:Xq}),Pq=L({meta:{key:"flag.rule_created",version:"1.0.0",description:"A targeting rule has been created.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","rule","create"]},payload:Yq}),Nq=L({meta:{key:"flag.rule_deleted",version:"1.0.0",description:"A targeting rule has been deleted.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","rule","delete"]},payload:Zq}),Wq=L({meta:{key:"experiment.created",version:"1.0.0",description:"An experiment has been created.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","experiment","create"]},payload:_q}),kq=L({meta:{key:"experiment.started",version:"1.0.0",description:"An experiment has started.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","experiment","start"]},payload:$q}),Cq=L({meta:{key:"experiment.stopped",version:"1.0.0",description:"An experiment has stopped.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","experiment","stop"]},payload:Gq}),Oq=L({meta:{key:"flag.evaluated",version:"1.0.0",description:"A feature flag has been evaluated.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","evaluate"]},payload:Lq}),Vq=L({meta:{key:"experiment.variant_assigned",version:"1.0.0",description:"A subject has been assigned to an experiment variant.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","experiment","variant"]},payload:Uq}),lq={FlagCreatedEvent:Aq,FlagUpdatedEvent:Qq,FlagDeletedEvent:Bq,FlagToggledEvent:Dq,RuleCreatedEvent:Pq,RuleDeletedEvent:Nq,ExperimentCreatedEvent:Wq,ExperimentStartedEvent:kq,ExperimentStoppedEvent:Cq,FlagEvaluatedEvent:Oq,VariantAssignedEvent:Vq};import{defineFeature as jq}from"@contractspec/lib.contracts-spec/features";var qw=jq({meta:{key:"libs.feature-flags",version:"1.0.0",title:"Feature Flags",description:"Feature flags and experiments module for ContractSpec applications",domain:"feature-flags",owners:["@contractspec-core"],tags:["package","libs","feature-flags"],stability:"experimental"},operations:[{key:"flag.create",version:"1.0.0"},{key:"flag.update",version:"1.0.0"},{key:"flag.delete",version:"1.0.0"},{key:"flag.toggle",version:"1.0.0"},{key:"flag.get",version:"1.0.0"},{key:"flag.list",version:"1.0.0"},{key:"flag.evaluate",version:"1.0.0"},{key:"flag.rule.create",version:"1.0.0"},{key:"flag.rule.delete",version:"1.0.0"},{key:"experiment.create",version:"1.0.0"},{key:"experiment.start",version:"1.0.0"},{key:"experiment.stop",version:"1.0.0"},{key:"experiment.get",version:"1.0.0"}],events:[{key:"flag.created",version:"1.0.0"},{key:"flag.updated",version:"1.0.0"},{key:"flag.deleted",version:"1.0.0"},{key:"flag.toggled",version:"1.0.0"},{key:"flag.rule_created",version:"1.0.0"},{key:"flag.rule_deleted",version:"1.0.0"},{key:"experiment.created",version:"1.0.0"},{key:"experiment.started",version:"1.0.0"},{key:"experiment.stopped",version:"1.0.0"},{key:"flag.evaluated",version:"1.0.0"},{key:"experiment.variant_assigned",version:"1.0.0"}]});export{k as hashToBucket,C as getSubjectId,cq as featureFlagsSchemaContribution,a as featureFlagEntities,e as evaluateRuleCondition,Vq as VariantAssignedEvent,vq as UpdateFlagContract,bq as ToggleFlagContract,I as TargetingRuleModel,Sq as StopExperimentContract,yq as StartExperimentContract,F as RuleOperatorEnum,Nq as RuleDeletedEvent,Pq as RuleCreatedEvent,sq as ListFlagsContract,zq as InMemoryFlagRepository,gq as GetFlagContract,Eq as GetExperimentContract,Qq as FlagUpdatedEvent,Dq as FlagToggledEvent,u as FlagTargetingRuleEntity,R as FlagStatusEnum,wq as FlagEvaluator,l as FlagEvaluationEntity,Oq as FlagEvaluatedEvent,Bq as FlagDeletedEvent,Aq as FlagCreatedEvent,qw as FeatureFlagsFeature,D as FeatureFlagModel,lq as FeatureFlagEvents,r as FeatureFlagEntity,Cq as ExperimentStoppedEvent,M as ExperimentStatusEnum,kq as ExperimentStartedEvent,W as ExperimentModel,t as ExperimentEntity,Wq as ExperimentCreatedEvent,n as ExperimentAssignmentEntity,v as EvaluationResultModel,xq as EvaluateFlagContract,fq as DeleteRuleContract,hq as DeleteFlagContract,oq as CreateRuleContract,Iq as CreateFlagContract,Tq as CreateExperimentContract};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{defineFeature as g}from"@contractspec/lib.contracts-spec";var j=g({meta:{key:"feature-flags",version:"1.0.0",title:"Feature Flags",description:"Feature
|
|
1
|
+
import{defineFeature as g}from"@contractspec/lib.contracts-spec/features";var j=g({meta:{key:"libs.feature-flags",version:"1.0.0",title:"Feature Flags",description:"Feature flags and experiments module for ContractSpec applications",domain:"feature-flags",owners:["@contractspec-core"],tags:["package","libs","feature-flags"],stability:"experimental"},operations:[{key:"flag.create",version:"1.0.0"},{key:"flag.update",version:"1.0.0"},{key:"flag.delete",version:"1.0.0"},{key:"flag.toggle",version:"1.0.0"},{key:"flag.get",version:"1.0.0"},{key:"flag.list",version:"1.0.0"},{key:"flag.evaluate",version:"1.0.0"},{key:"flag.rule.create",version:"1.0.0"},{key:"flag.rule.delete",version:"1.0.0"},{key:"experiment.create",version:"1.0.0"},{key:"experiment.start",version:"1.0.0"},{key:"experiment.stop",version:"1.0.0"},{key:"experiment.get",version:"1.0.0"}],events:[{key:"flag.created",version:"1.0.0"},{key:"flag.updated",version:"1.0.0"},{key:"flag.deleted",version:"1.0.0"},{key:"flag.toggled",version:"1.0.0"},{key:"flag.rule_created",version:"1.0.0"},{key:"flag.rule_deleted",version:"1.0.0"},{key:"experiment.created",version:"1.0.0"},{key:"experiment.started",version:"1.0.0"},{key:"experiment.stopped",version:"1.0.0"},{key:"flag.evaluated",version:"1.0.0"},{key:"experiment.variant_assigned",version:"1.0.0"}]});export{j as FeatureFlagsFeature};
|
package/dist/node/index.js
CHANGED
|
@@ -54,4 +54,4 @@ ${"```"},
|
|
|
54
54
|
- Ensure experiments’ variant percentages sum to 100; default flag status to OFF.
|
|
55
55
|
- Use org-scoped flags for multi-tenant isolation.
|
|
56
56
|
- Log evaluations only when needed to control volume; prefer sampling for noisy paths.
|
|
57
|
-
`}];i(c);import{defineEntity as P,defineEntityEnum as V,field as z,index as $}from"@contractspec/lib.schema";var R=V({name:"FlagStatus",values:["OFF","ON","GRADUAL"],schema:"lssm_feature_flags",description:"Status of a feature flag."}),F=V({name:"RuleOperator",values:["EQ","NEQ","IN","NIN","CONTAINS","NOT_CONTAINS","GT","GTE","LT","LTE","PERCENTAGE"],schema:"lssm_feature_flags",description:"Operator for targeting rule conditions."}),M=V({name:"ExperimentStatus",values:["DRAFT","RUNNING","PAUSED","COMPLETED","CANCELLED"],schema:"lssm_feature_flags",description:"Status of an experiment."}),r=P({name:"FeatureFlag",description:"A feature flag for controlling feature availability.",schema:"lssm_feature_flags",map:"feature_flag",fields:{id:z.id({description:"Unique flag identifier"}),key:z.string({isUnique:!0,description:"Flag key (e.g., new_dashboard)"}),name:z.string({description:"Human-readable name"}),description:z.string({isOptional:!0,description:"Description of the flag"}),status:z.enum("FlagStatus",{default:"OFF",description:"Flag status"}),defaultValue:z.boolean({default:!1,description:"Default value when no rules match"}),variants:z.json({isOptional:!0,description:"Variant definitions for multivariate flags"}),orgId:z.string({isOptional:!0,description:"Organization scope (null = global)"}),tags:z.json({isOptional:!0,description:"Tags for categorization"}),metadata:z.json({isOptional:!0,description:"Additional metadata"}),createdAt:z.createdAt(),updatedAt:z.updatedAt(),targetingRules:z.hasMany("FlagTargetingRule"),experiments:z.hasMany("Experiment"),evaluations:z.hasMany("FlagEvaluation")},indexes:[$.on(["orgId","key"]),$.on(["status"])],enums:[R]}),u=P({name:"FlagTargetingRule",description:"A targeting rule for conditional flag evaluation.",schema:"lssm_feature_flags",map:"flag_targeting_rule",fields:{id:z.id({description:"Unique rule identifier"}),flagId:z.foreignKey({description:"Parent feature flag"}),name:z.string({isOptional:!0,description:"Rule name for debugging"}),priority:z.int({default:0,description:"Rule priority (lower = higher priority)"}),enabled:z.boolean({default:!0,description:"Whether rule is active"}),attribute:z.string({description:"Target attribute (userId, orgId, plan, segment, etc.)"}),operator:z.enum("RuleOperator",{description:"Comparison operator"}),value:z.json({description:"Target value(s)"}),rolloutPercentage:z.int({isOptional:!0,description:"Percentage for gradual rollout (0-100)"}),serveValue:z.boolean({isOptional:!0,description:"Boolean value to serve"}),serveVariant:z.string({isOptional:!0,description:"Variant key to serve (for multivariate)"}),createdAt:z.createdAt(),updatedAt:z.updatedAt(),flag:z.belongsTo("FeatureFlag",["flagId"],["id"],{onDelete:"Cascade"})},indexes:[$.on(["flagId","priority"]),$.on(["attribute"])],enums:[F]}),t=P({name:"Experiment",description:"An A/B test experiment.",schema:"lssm_feature_flags",map:"experiment",fields:{id:z.id({description:"Unique experiment identifier"}),key:z.string({isUnique:!0,description:"Experiment key"}),name:z.string({description:"Human-readable name"}),description:z.string({isOptional:!0,description:"Experiment description"}),hypothesis:z.string({isOptional:!0,description:"Experiment hypothesis"}),flagId:z.foreignKey({description:"Associated feature flag"}),status:z.enum("ExperimentStatus",{default:"DRAFT",description:"Experiment status"}),variants:z.json({description:"Variant definitions with split ratios"}),metrics:z.json({isOptional:!0,description:"Metrics to track"}),audiencePercentage:z.int({default:100,description:"Percentage of audience to include"}),audienceFilter:z.json({isOptional:!0,description:"Audience filter criteria"}),scheduledStartAt:z.dateTime({isOptional:!0,description:"Scheduled start time"}),scheduledEndAt:z.dateTime({isOptional:!0,description:"Scheduled end time"}),startedAt:z.dateTime({isOptional:!0,description:"Actual start time"}),endedAt:z.dateTime({isOptional:!0,description:"Actual end time"}),winningVariant:z.string({isOptional:!0,description:"Declared winning variant"}),results:z.json({isOptional:!0,description:"Experiment results summary"}),orgId:z.string({isOptional:!0,description:"Organization scope"}),createdAt:z.createdAt(),updatedAt:z.updatedAt(),flag:z.belongsTo("FeatureFlag",["flagId"],["id"],{onDelete:"Cascade"}),assignments:z.hasMany("ExperimentAssignment")},indexes:[$.on(["status"]),$.on(["orgId","status"]),$.on(["flagId"])],enums:[M]}),n=P({name:"ExperimentAssignment",description:"Tracks experiment variant assignments.",schema:"lssm_feature_flags",map:"experiment_assignment",fields:{id:z.id({description:"Unique assignment identifier"}),experimentId:z.foreignKey({description:"Parent experiment"}),subjectType:z.string({description:"Subject type (user, org, session)"}),subjectId:z.string({description:"Subject identifier"}),variant:z.string({description:"Assigned variant key"}),bucket:z.int({description:"Hash bucket (0-99)"}),context:z.json({isOptional:!0,description:"Context at assignment time"}),assignedAt:z.dateTime({description:"Assignment timestamp"}),experiment:z.belongsTo("Experiment",["experimentId"],["id"],{onDelete:"Cascade"})},indexes:[$.unique(["experimentId","subjectType","subjectId"],{name:"experiment_assignment_unique"}),$.on(["subjectType","subjectId"])]}),l=P({name:"FlagEvaluation",description:"Log of flag evaluations for debugging and analytics.",schema:"lssm_feature_flags",map:"flag_evaluation",fields:{id:z.id({description:"Unique evaluation identifier"}),flagId:z.foreignKey({description:"Evaluated flag"}),flagKey:z.string({description:"Flag key (denormalized for queries)"}),subjectType:z.string({description:"Subject type (user, org, anonymous)"}),subjectId:z.string({description:"Subject identifier"}),result:z.boolean({description:"Evaluation result"}),variant:z.string({isOptional:!0,description:"Served variant (for multivariate)"}),matchedRuleId:z.string({isOptional:!0,description:"Rule that matched (if any)"}),reason:z.string({description:"Evaluation reason (default, rule, experiment, etc.)"}),context:z.json({isOptional:!0,description:"Evaluation context"}),evaluatedAt:z.dateTime({description:"Evaluation timestamp"}),flag:z.belongsTo("FeatureFlag",["flagId"],["id"],{onDelete:"Cascade"})},indexes:[$.on(["flagKey","evaluatedAt"]),$.on(["subjectType","subjectId","evaluatedAt"]),$.on(["flagId","evaluatedAt"])]}),a=[r,u,t,n,l],cq={moduleId:"@contractspec/lib.feature-flags",entities:a,enums:[R,F,M]};function k(w,K=""){let J=`${K}:${w}`,X=0;for(let G=0;G<J.length;G++){let A=J.charCodeAt(G);X=(X<<5)-X+A,X=X&X}return Math.abs(X%100)}function C(w){return w.userId||w.sessionId||w.orgId||"anonymous"}function e(w,K){let J=qq(w.attribute,K);switch(w.operator){case"EQ":return J===w.value;case"NEQ":return J!==w.value;case"IN":if(!Array.isArray(w.value))return!1;return w.value.includes(J);case"NIN":if(!Array.isArray(w.value))return!0;return!w.value.includes(J);case"CONTAINS":if(typeof J!=="string"||typeof w.value!=="string")return!1;return J.includes(w.value);case"NOT_CONTAINS":if(typeof J!=="string"||typeof w.value!=="string")return!0;return!J.includes(w.value);case"GT":if(typeof J!=="number"||typeof w.value!=="number")return!1;return J>w.value;case"GTE":if(typeof J!=="number"||typeof w.value!=="number")return!1;return J>=w.value;case"LT":if(typeof J!=="number"||typeof w.value!=="number")return!1;return J<w.value;case"LTE":if(typeof J!=="number"||typeof w.value!=="number")return!1;return J<=w.value;case"PERCENTAGE":return k(C(K),w.attribute)<(typeof w.value==="number"?w.value:0);default:return!1}}function qq(w,K){switch(w){case"userId":return K.userId;case"orgId":return K.orgId;case"plan":return K.plan;case"segment":return K.segment;case"sessionId":return K.sessionId;default:return K.attributes?.[w]}}class wq{repository;logger;logEvaluations;constructor(w){this.repository=w.repository,this.logger=w.logger,this.logEvaluations=w.logEvaluations??!1}async evaluate(w,K){let J=K.orgId,X=await this.repository.getFlag(w,J);if(!X)return this.makeResult(!1,"FLAG_NOT_FOUND");if(X.status==="OFF")return this.logAndReturn(X,K,this.makeResult(!1,"FLAG_OFF"));if(X.status==="ON")return this.logAndReturn(X,K,this.makeResult(!0,"FLAG_ON"));let A=[...await this.repository.getRules(X.id)].filter((Z)=>Z.enabled).sort((Z,O)=>Z.priority-O.priority);for(let Z of A)if(e(Z,K)){if(Z.rolloutPercentage!==void 0&&Z.rolloutPercentage!==null){if(k(C(K),X.key)>=Z.rolloutPercentage)continue}let O=Z.serveValue??!0;return this.logAndReturn(X,K,this.makeResult(O,"RULE_MATCH",Z.serveVariant,Z.id))}let B=await this.repository.getActiveExperiment(X.id);if(B&&B.status==="RUNNING"){let Z=await this.evaluateExperiment(B,K);if(Z)return this.logAndReturn(X,K,Z)}return this.logAndReturn(X,K,this.makeResult(X.defaultValue,"DEFAULT_VALUE"))}async evaluateExperiment(w,K){let J=C(K),X=K.userId?"user":K.orgId?"org":"session";if(k(J,`${w.key}:audience`)>=w.audiencePercentage)return null;let A=await this.repository.getExperimentAssignment(w.id,X,J);if(!A){let Z=k(J,w.key);A=this.assignVariant(w.variants,Z),await this.repository.saveExperimentAssignment(w.id,X,J,A,Z)}let B=A!=="control";return this.makeResult(B,"EXPERIMENT_VARIANT",A,void 0,w.id)}assignVariant(w,K){let J=0;for(let X of w)if(J+=X.percentage,K<J)return X.key;return w[w.length-1]?.key??"control"}makeResult(w,K,J,X,G){return{enabled:w,variant:J,reason:K,ruleId:X,experimentId:G}}logAndReturn(w,K,J){if(this.logEvaluations&&this.logger){let X=C(K),G=K.userId?"user":K.orgId?"org":"session";this.logger.log({flagId:w.id,flagKey:w.key,subjectType:G,subjectId:X,result:J.enabled,variant:J.variant,reason:J.reason,ruleId:J.ruleId,experimentId:J.experimentId,context:K})}return J}}class zq{flags=new Map;rules=new Map;experiments=new Map;assignments=new Map;addFlag(w){this.flags.set(w.key,w)}addRule(w,K){let J=this.rules.get(w)||[];J.push(K),this.rules.set(w,J)}addExperiment(w,K){this.experiments.set(K,w)}async getFlag(w){return this.flags.get(w)||null}async getRules(w){return this.rules.get(w)||[]}async getActiveExperiment(w){return this.experiments.get(w)||null}async getExperimentAssignment(w,K,J){let X=`${w}:${K}:${J}`;return this.assignments.get(X)||null}async saveExperimentAssignment(w,K,J,X){let G=`${w}:${K}:${J}`;this.assignments.set(G,X)}clear(){this.flags.clear(),this.rules.clear(),this.experiments.clear(),this.assignments.clear()}}import{defineEvent as L}from"@contractspec/lib.contracts-spec";import{defineSchemaModel as U,ScalarTypeEnum as H}from"@contractspec/lib.schema";var Hq=U({name:"FlagCreatedEventPayload",description:"Payload when a feature flag is created",fields:{flagId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},name:{type:H.String_unsecure(),isOptional:!1},status:{type:H.String_unsecure(),isOptional:!1},orgId:{type:H.String_unsecure(),isOptional:!0},createdBy:{type:H.String_unsecure(),isOptional:!0},createdAt:{type:H.DateTime(),isOptional:!1}}}),Jq=U({name:"FlagUpdatedEventPayload",description:"Payload when a feature flag is updated",fields:{flagId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},changes:{type:H.JSON(),isOptional:!1},updatedBy:{type:H.String_unsecure(),isOptional:!0},updatedAt:{type:H.DateTime(),isOptional:!1}}}),Kq=U({name:"FlagDeletedEventPayload",description:"Payload when a feature flag is deleted",fields:{flagId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},deletedBy:{type:H.String_unsecure(),isOptional:!0},deletedAt:{type:H.DateTime(),isOptional:!1}}}),Xq=U({name:"FlagToggledEventPayload",description:"Payload when a feature flag status is toggled",fields:{flagId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},previousStatus:{type:H.String_unsecure(),isOptional:!1},newStatus:{type:H.String_unsecure(),isOptional:!1},toggledBy:{type:H.String_unsecure(),isOptional:!0},toggledAt:{type:H.DateTime(),isOptional:!1}}}),Yq=U({name:"RuleCreatedEventPayload",description:"Payload when a targeting rule is created",fields:{ruleId:{type:H.String_unsecure(),isOptional:!1},flagId:{type:H.String_unsecure(),isOptional:!1},flagKey:{type:H.String_unsecure(),isOptional:!1},attribute:{type:H.String_unsecure(),isOptional:!1},operator:{type:H.String_unsecure(),isOptional:!1},createdAt:{type:H.DateTime(),isOptional:!1}}}),Zq=U({name:"RuleDeletedEventPayload",description:"Payload when a targeting rule is deleted",fields:{ruleId:{type:H.String_unsecure(),isOptional:!1},flagId:{type:H.String_unsecure(),isOptional:!1},flagKey:{type:H.String_unsecure(),isOptional:!1},deletedAt:{type:H.DateTime(),isOptional:!1}}}),_q=U({name:"ExperimentCreatedEventPayload",description:"Payload when an experiment is created",fields:{experimentId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},name:{type:H.String_unsecure(),isOptional:!1},flagId:{type:H.String_unsecure(),isOptional:!1},variants:{type:H.JSON(),isOptional:!1},createdBy:{type:H.String_unsecure(),isOptional:!0},createdAt:{type:H.DateTime(),isOptional:!1}}}),$q=U({name:"ExperimentStartedEventPayload",description:"Payload when an experiment starts",fields:{experimentId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},flagId:{type:H.String_unsecure(),isOptional:!1},variants:{type:H.JSON(),isOptional:!1},audiencePercentage:{type:H.Int_unsecure(),isOptional:!1},startedBy:{type:H.String_unsecure(),isOptional:!0},startedAt:{type:H.DateTime(),isOptional:!1}}}),Gq=U({name:"ExperimentStoppedEventPayload",description:"Payload when an experiment stops",fields:{experimentId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},reason:{type:H.String_unsecure(),isOptional:!1},winningVariant:{type:H.String_unsecure(),isOptional:!0},stoppedBy:{type:H.String_unsecure(),isOptional:!0},stoppedAt:{type:H.DateTime(),isOptional:!1}}}),Lq=U({name:"FlagEvaluatedEventPayload",description:"Payload when a flag is evaluated (for analytics)",fields:{flagId:{type:H.String_unsecure(),isOptional:!1},flagKey:{type:H.String_unsecure(),isOptional:!1},subjectType:{type:H.String_unsecure(),isOptional:!1},subjectId:{type:H.String_unsecure(),isOptional:!1},result:{type:H.Boolean(),isOptional:!1},variant:{type:H.String_unsecure(),isOptional:!0},reason:{type:H.String_unsecure(),isOptional:!1},evaluatedAt:{type:H.DateTime(),isOptional:!1}}}),Uq=U({name:"VariantAssignedEventPayload",description:"Payload when a subject is assigned to an experiment variant",fields:{experimentId:{type:H.String_unsecure(),isOptional:!1},experimentKey:{type:H.String_unsecure(),isOptional:!1},subjectType:{type:H.String_unsecure(),isOptional:!1},subjectId:{type:H.String_unsecure(),isOptional:!1},variant:{type:H.String_unsecure(),isOptional:!1},bucket:{type:H.Int_unsecure(),isOptional:!1},assignedAt:{type:H.DateTime(),isOptional:!1}}}),Aq=L({meta:{key:"flag.created",version:"1.0.0",description:"A feature flag has been created.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","create"]},payload:Hq}),Qq=L({meta:{key:"flag.updated",version:"1.0.0",description:"A feature flag has been updated.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","update"]},payload:Jq}),Bq=L({meta:{key:"flag.deleted",version:"1.0.0",description:"A feature flag has been deleted.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","delete"]},payload:Kq}),Dq=L({meta:{key:"flag.toggled",version:"1.0.0",description:"A feature flag status has been toggled.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","toggle"]},payload:Xq}),Pq=L({meta:{key:"flag.rule_created",version:"1.0.0",description:"A targeting rule has been created.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","rule","create"]},payload:Yq}),Nq=L({meta:{key:"flag.rule_deleted",version:"1.0.0",description:"A targeting rule has been deleted.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","rule","delete"]},payload:Zq}),Wq=L({meta:{key:"experiment.created",version:"1.0.0",description:"An experiment has been created.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","experiment","create"]},payload:_q}),kq=L({meta:{key:"experiment.started",version:"1.0.0",description:"An experiment has started.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","experiment","start"]},payload:$q}),Cq=L({meta:{key:"experiment.stopped",version:"1.0.0",description:"An experiment has stopped.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","experiment","stop"]},payload:Gq}),Oq=L({meta:{key:"flag.evaluated",version:"1.0.0",description:"A feature flag has been evaluated.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","evaluate"]},payload:Lq}),Vq=L({meta:{key:"experiment.variant_assigned",version:"1.0.0",description:"A subject has been assigned to an experiment variant.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","experiment","variant"]},payload:Uq}),lq={FlagCreatedEvent:Aq,FlagUpdatedEvent:Qq,FlagDeletedEvent:Bq,FlagToggledEvent:Dq,RuleCreatedEvent:Pq,RuleDeletedEvent:Nq,ExperimentCreatedEvent:Wq,ExperimentStartedEvent:kq,ExperimentStoppedEvent:Cq,FlagEvaluatedEvent:Oq,VariantAssignedEvent:Vq};import{defineFeature as jq}from"@contractspec/lib.contracts-spec";var qw=jq({meta:{key:"feature-flags",version:"1.0.0",title:"Feature Flags",description:"Feature flag management with targeting rules and A/B experiments",domain:"platform",owners:["@platform.feature-flags"],tags:["feature-flags","experiments","targeting"],stability:"stable"},operations:[{key:"flag.create",version:"1.0.0"},{key:"flag.update",version:"1.0.0"},{key:"flag.delete",version:"1.0.0"},{key:"flag.toggle",version:"1.0.0"},{key:"flag.get",version:"1.0.0"},{key:"flag.list",version:"1.0.0"},{key:"flag.evaluate",version:"1.0.0"},{key:"flag.rule.create",version:"1.0.0"},{key:"flag.rule.delete",version:"1.0.0"},{key:"experiment.create",version:"1.0.0"},{key:"experiment.start",version:"1.0.0"},{key:"experiment.stop",version:"1.0.0"},{key:"experiment.get",version:"1.0.0"}],events:[{key:"flag.created",version:"1.0.0"},{key:"flag.updated",version:"1.0.0"},{key:"flag.deleted",version:"1.0.0"},{key:"flag.toggled",version:"1.0.0"},{key:"flag.evaluated",version:"1.0.0"},{key:"flag.rule_created",version:"1.0.0"},{key:"flag.rule_deleted",version:"1.0.0"},{key:"experiment.created",version:"1.0.0"},{key:"experiment.started",version:"1.0.0"},{key:"experiment.stopped",version:"1.0.0"},{key:"experiment.variant_assigned",version:"1.0.0"}],presentations:[],opToPresentation:[],presentationsTargets:[],capabilities:{provides:[{key:"feature-flag",version:"1.0.0"},{key:"experiments",version:"1.0.0"}],requires:[{key:"identity",version:"1.0.0"}]}});export{k as hashToBucket,C as getSubjectId,cq as featureFlagsSchemaContribution,a as featureFlagEntities,e as evaluateRuleCondition,Vq as VariantAssignedEvent,vq as UpdateFlagContract,bq as ToggleFlagContract,I as TargetingRuleModel,Sq as StopExperimentContract,yq as StartExperimentContract,F as RuleOperatorEnum,Nq as RuleDeletedEvent,Pq as RuleCreatedEvent,sq as ListFlagsContract,zq as InMemoryFlagRepository,gq as GetFlagContract,Eq as GetExperimentContract,Qq as FlagUpdatedEvent,Dq as FlagToggledEvent,u as FlagTargetingRuleEntity,R as FlagStatusEnum,wq as FlagEvaluator,l as FlagEvaluationEntity,Oq as FlagEvaluatedEvent,Bq as FlagDeletedEvent,Aq as FlagCreatedEvent,qw as FeatureFlagsFeature,D as FeatureFlagModel,lq as FeatureFlagEvents,r as FeatureFlagEntity,Cq as ExperimentStoppedEvent,M as ExperimentStatusEnum,kq as ExperimentStartedEvent,W as ExperimentModel,t as ExperimentEntity,Wq as ExperimentCreatedEvent,n as ExperimentAssignmentEntity,v as EvaluationResultModel,xq as EvaluateFlagContract,fq as DeleteRuleContract,hq as DeleteFlagContract,oq as CreateRuleContract,Iq as CreateFlagContract,Tq as CreateExperimentContract};
|
|
57
|
+
`}];i(c);import{defineEntity as P,defineEntityEnum as V,field as z,index as $}from"@contractspec/lib.schema";var R=V({name:"FlagStatus",values:["OFF","ON","GRADUAL"],schema:"lssm_feature_flags",description:"Status of a feature flag."}),F=V({name:"RuleOperator",values:["EQ","NEQ","IN","NIN","CONTAINS","NOT_CONTAINS","GT","GTE","LT","LTE","PERCENTAGE"],schema:"lssm_feature_flags",description:"Operator for targeting rule conditions."}),M=V({name:"ExperimentStatus",values:["DRAFT","RUNNING","PAUSED","COMPLETED","CANCELLED"],schema:"lssm_feature_flags",description:"Status of an experiment."}),r=P({name:"FeatureFlag",description:"A feature flag for controlling feature availability.",schema:"lssm_feature_flags",map:"feature_flag",fields:{id:z.id({description:"Unique flag identifier"}),key:z.string({isUnique:!0,description:"Flag key (e.g., new_dashboard)"}),name:z.string({description:"Human-readable name"}),description:z.string({isOptional:!0,description:"Description of the flag"}),status:z.enum("FlagStatus",{default:"OFF",description:"Flag status"}),defaultValue:z.boolean({default:!1,description:"Default value when no rules match"}),variants:z.json({isOptional:!0,description:"Variant definitions for multivariate flags"}),orgId:z.string({isOptional:!0,description:"Organization scope (null = global)"}),tags:z.json({isOptional:!0,description:"Tags for categorization"}),metadata:z.json({isOptional:!0,description:"Additional metadata"}),createdAt:z.createdAt(),updatedAt:z.updatedAt(),targetingRules:z.hasMany("FlagTargetingRule"),experiments:z.hasMany("Experiment"),evaluations:z.hasMany("FlagEvaluation")},indexes:[$.on(["orgId","key"]),$.on(["status"])],enums:[R]}),u=P({name:"FlagTargetingRule",description:"A targeting rule for conditional flag evaluation.",schema:"lssm_feature_flags",map:"flag_targeting_rule",fields:{id:z.id({description:"Unique rule identifier"}),flagId:z.foreignKey({description:"Parent feature flag"}),name:z.string({isOptional:!0,description:"Rule name for debugging"}),priority:z.int({default:0,description:"Rule priority (lower = higher priority)"}),enabled:z.boolean({default:!0,description:"Whether rule is active"}),attribute:z.string({description:"Target attribute (userId, orgId, plan, segment, etc.)"}),operator:z.enum("RuleOperator",{description:"Comparison operator"}),value:z.json({description:"Target value(s)"}),rolloutPercentage:z.int({isOptional:!0,description:"Percentage for gradual rollout (0-100)"}),serveValue:z.boolean({isOptional:!0,description:"Boolean value to serve"}),serveVariant:z.string({isOptional:!0,description:"Variant key to serve (for multivariate)"}),createdAt:z.createdAt(),updatedAt:z.updatedAt(),flag:z.belongsTo("FeatureFlag",["flagId"],["id"],{onDelete:"Cascade"})},indexes:[$.on(["flagId","priority"]),$.on(["attribute"])],enums:[F]}),t=P({name:"Experiment",description:"An A/B test experiment.",schema:"lssm_feature_flags",map:"experiment",fields:{id:z.id({description:"Unique experiment identifier"}),key:z.string({isUnique:!0,description:"Experiment key"}),name:z.string({description:"Human-readable name"}),description:z.string({isOptional:!0,description:"Experiment description"}),hypothesis:z.string({isOptional:!0,description:"Experiment hypothesis"}),flagId:z.foreignKey({description:"Associated feature flag"}),status:z.enum("ExperimentStatus",{default:"DRAFT",description:"Experiment status"}),variants:z.json({description:"Variant definitions with split ratios"}),metrics:z.json({isOptional:!0,description:"Metrics to track"}),audiencePercentage:z.int({default:100,description:"Percentage of audience to include"}),audienceFilter:z.json({isOptional:!0,description:"Audience filter criteria"}),scheduledStartAt:z.dateTime({isOptional:!0,description:"Scheduled start time"}),scheduledEndAt:z.dateTime({isOptional:!0,description:"Scheduled end time"}),startedAt:z.dateTime({isOptional:!0,description:"Actual start time"}),endedAt:z.dateTime({isOptional:!0,description:"Actual end time"}),winningVariant:z.string({isOptional:!0,description:"Declared winning variant"}),results:z.json({isOptional:!0,description:"Experiment results summary"}),orgId:z.string({isOptional:!0,description:"Organization scope"}),createdAt:z.createdAt(),updatedAt:z.updatedAt(),flag:z.belongsTo("FeatureFlag",["flagId"],["id"],{onDelete:"Cascade"}),assignments:z.hasMany("ExperimentAssignment")},indexes:[$.on(["status"]),$.on(["orgId","status"]),$.on(["flagId"])],enums:[M]}),n=P({name:"ExperimentAssignment",description:"Tracks experiment variant assignments.",schema:"lssm_feature_flags",map:"experiment_assignment",fields:{id:z.id({description:"Unique assignment identifier"}),experimentId:z.foreignKey({description:"Parent experiment"}),subjectType:z.string({description:"Subject type (user, org, session)"}),subjectId:z.string({description:"Subject identifier"}),variant:z.string({description:"Assigned variant key"}),bucket:z.int({description:"Hash bucket (0-99)"}),context:z.json({isOptional:!0,description:"Context at assignment time"}),assignedAt:z.dateTime({description:"Assignment timestamp"}),experiment:z.belongsTo("Experiment",["experimentId"],["id"],{onDelete:"Cascade"})},indexes:[$.unique(["experimentId","subjectType","subjectId"],{name:"experiment_assignment_unique"}),$.on(["subjectType","subjectId"])]}),l=P({name:"FlagEvaluation",description:"Log of flag evaluations for debugging and analytics.",schema:"lssm_feature_flags",map:"flag_evaluation",fields:{id:z.id({description:"Unique evaluation identifier"}),flagId:z.foreignKey({description:"Evaluated flag"}),flagKey:z.string({description:"Flag key (denormalized for queries)"}),subjectType:z.string({description:"Subject type (user, org, anonymous)"}),subjectId:z.string({description:"Subject identifier"}),result:z.boolean({description:"Evaluation result"}),variant:z.string({isOptional:!0,description:"Served variant (for multivariate)"}),matchedRuleId:z.string({isOptional:!0,description:"Rule that matched (if any)"}),reason:z.string({description:"Evaluation reason (default, rule, experiment, etc.)"}),context:z.json({isOptional:!0,description:"Evaluation context"}),evaluatedAt:z.dateTime({description:"Evaluation timestamp"}),flag:z.belongsTo("FeatureFlag",["flagId"],["id"],{onDelete:"Cascade"})},indexes:[$.on(["flagKey","evaluatedAt"]),$.on(["subjectType","subjectId","evaluatedAt"]),$.on(["flagId","evaluatedAt"])]}),a=[r,u,t,n,l],cq={moduleId:"@contractspec/lib.feature-flags",entities:a,enums:[R,F,M]};function k(w,K=""){let J=`${K}:${w}`,X=0;for(let G=0;G<J.length;G++){let A=J.charCodeAt(G);X=(X<<5)-X+A,X=X&X}return Math.abs(X%100)}function C(w){return w.userId||w.sessionId||w.orgId||"anonymous"}function e(w,K){let J=qq(w.attribute,K);switch(w.operator){case"EQ":return J===w.value;case"NEQ":return J!==w.value;case"IN":if(!Array.isArray(w.value))return!1;return w.value.includes(J);case"NIN":if(!Array.isArray(w.value))return!0;return!w.value.includes(J);case"CONTAINS":if(typeof J!=="string"||typeof w.value!=="string")return!1;return J.includes(w.value);case"NOT_CONTAINS":if(typeof J!=="string"||typeof w.value!=="string")return!0;return!J.includes(w.value);case"GT":if(typeof J!=="number"||typeof w.value!=="number")return!1;return J>w.value;case"GTE":if(typeof J!=="number"||typeof w.value!=="number")return!1;return J>=w.value;case"LT":if(typeof J!=="number"||typeof w.value!=="number")return!1;return J<w.value;case"LTE":if(typeof J!=="number"||typeof w.value!=="number")return!1;return J<=w.value;case"PERCENTAGE":return k(C(K),w.attribute)<(typeof w.value==="number"?w.value:0);default:return!1}}function qq(w,K){switch(w){case"userId":return K.userId;case"orgId":return K.orgId;case"plan":return K.plan;case"segment":return K.segment;case"sessionId":return K.sessionId;default:return K.attributes?.[w]}}class wq{repository;logger;logEvaluations;constructor(w){this.repository=w.repository,this.logger=w.logger,this.logEvaluations=w.logEvaluations??!1}async evaluate(w,K){let J=K.orgId,X=await this.repository.getFlag(w,J);if(!X)return this.makeResult(!1,"FLAG_NOT_FOUND");if(X.status==="OFF")return this.logAndReturn(X,K,this.makeResult(!1,"FLAG_OFF"));if(X.status==="ON")return this.logAndReturn(X,K,this.makeResult(!0,"FLAG_ON"));let A=[...await this.repository.getRules(X.id)].filter((Z)=>Z.enabled).sort((Z,O)=>Z.priority-O.priority);for(let Z of A)if(e(Z,K)){if(Z.rolloutPercentage!==void 0&&Z.rolloutPercentage!==null){if(k(C(K),X.key)>=Z.rolloutPercentage)continue}let O=Z.serveValue??!0;return this.logAndReturn(X,K,this.makeResult(O,"RULE_MATCH",Z.serveVariant,Z.id))}let B=await this.repository.getActiveExperiment(X.id);if(B&&B.status==="RUNNING"){let Z=await this.evaluateExperiment(B,K);if(Z)return this.logAndReturn(X,K,Z)}return this.logAndReturn(X,K,this.makeResult(X.defaultValue,"DEFAULT_VALUE"))}async evaluateExperiment(w,K){let J=C(K),X=K.userId?"user":K.orgId?"org":"session";if(k(J,`${w.key}:audience`)>=w.audiencePercentage)return null;let A=await this.repository.getExperimentAssignment(w.id,X,J);if(!A){let Z=k(J,w.key);A=this.assignVariant(w.variants,Z),await this.repository.saveExperimentAssignment(w.id,X,J,A,Z)}let B=A!=="control";return this.makeResult(B,"EXPERIMENT_VARIANT",A,void 0,w.id)}assignVariant(w,K){let J=0;for(let X of w)if(J+=X.percentage,K<J)return X.key;return w[w.length-1]?.key??"control"}makeResult(w,K,J,X,G){return{enabled:w,variant:J,reason:K,ruleId:X,experimentId:G}}logAndReturn(w,K,J){if(this.logEvaluations&&this.logger){let X=C(K),G=K.userId?"user":K.orgId?"org":"session";this.logger.log({flagId:w.id,flagKey:w.key,subjectType:G,subjectId:X,result:J.enabled,variant:J.variant,reason:J.reason,ruleId:J.ruleId,experimentId:J.experimentId,context:K})}return J}}class zq{flags=new Map;rules=new Map;experiments=new Map;assignments=new Map;addFlag(w){this.flags.set(w.key,w)}addRule(w,K){let J=this.rules.get(w)||[];J.push(K),this.rules.set(w,J)}addExperiment(w,K){this.experiments.set(K,w)}async getFlag(w){return this.flags.get(w)||null}async getRules(w){return this.rules.get(w)||[]}async getActiveExperiment(w){return this.experiments.get(w)||null}async getExperimentAssignment(w,K,J){let X=`${w}:${K}:${J}`;return this.assignments.get(X)||null}async saveExperimentAssignment(w,K,J,X){let G=`${w}:${K}:${J}`;this.assignments.set(G,X)}clear(){this.flags.clear(),this.rules.clear(),this.experiments.clear(),this.assignments.clear()}}import{defineEvent as L}from"@contractspec/lib.contracts-spec";import{defineSchemaModel as U,ScalarTypeEnum as H}from"@contractspec/lib.schema";var Hq=U({name:"FlagCreatedEventPayload",description:"Payload when a feature flag is created",fields:{flagId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},name:{type:H.String_unsecure(),isOptional:!1},status:{type:H.String_unsecure(),isOptional:!1},orgId:{type:H.String_unsecure(),isOptional:!0},createdBy:{type:H.String_unsecure(),isOptional:!0},createdAt:{type:H.DateTime(),isOptional:!1}}}),Jq=U({name:"FlagUpdatedEventPayload",description:"Payload when a feature flag is updated",fields:{flagId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},changes:{type:H.JSON(),isOptional:!1},updatedBy:{type:H.String_unsecure(),isOptional:!0},updatedAt:{type:H.DateTime(),isOptional:!1}}}),Kq=U({name:"FlagDeletedEventPayload",description:"Payload when a feature flag is deleted",fields:{flagId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},deletedBy:{type:H.String_unsecure(),isOptional:!0},deletedAt:{type:H.DateTime(),isOptional:!1}}}),Xq=U({name:"FlagToggledEventPayload",description:"Payload when a feature flag status is toggled",fields:{flagId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},previousStatus:{type:H.String_unsecure(),isOptional:!1},newStatus:{type:H.String_unsecure(),isOptional:!1},toggledBy:{type:H.String_unsecure(),isOptional:!0},toggledAt:{type:H.DateTime(),isOptional:!1}}}),Yq=U({name:"RuleCreatedEventPayload",description:"Payload when a targeting rule is created",fields:{ruleId:{type:H.String_unsecure(),isOptional:!1},flagId:{type:H.String_unsecure(),isOptional:!1},flagKey:{type:H.String_unsecure(),isOptional:!1},attribute:{type:H.String_unsecure(),isOptional:!1},operator:{type:H.String_unsecure(),isOptional:!1},createdAt:{type:H.DateTime(),isOptional:!1}}}),Zq=U({name:"RuleDeletedEventPayload",description:"Payload when a targeting rule is deleted",fields:{ruleId:{type:H.String_unsecure(),isOptional:!1},flagId:{type:H.String_unsecure(),isOptional:!1},flagKey:{type:H.String_unsecure(),isOptional:!1},deletedAt:{type:H.DateTime(),isOptional:!1}}}),_q=U({name:"ExperimentCreatedEventPayload",description:"Payload when an experiment is created",fields:{experimentId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},name:{type:H.String_unsecure(),isOptional:!1},flagId:{type:H.String_unsecure(),isOptional:!1},variants:{type:H.JSON(),isOptional:!1},createdBy:{type:H.String_unsecure(),isOptional:!0},createdAt:{type:H.DateTime(),isOptional:!1}}}),$q=U({name:"ExperimentStartedEventPayload",description:"Payload when an experiment starts",fields:{experimentId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},flagId:{type:H.String_unsecure(),isOptional:!1},variants:{type:H.JSON(),isOptional:!1},audiencePercentage:{type:H.Int_unsecure(),isOptional:!1},startedBy:{type:H.String_unsecure(),isOptional:!0},startedAt:{type:H.DateTime(),isOptional:!1}}}),Gq=U({name:"ExperimentStoppedEventPayload",description:"Payload when an experiment stops",fields:{experimentId:{type:H.String_unsecure(),isOptional:!1},key:{type:H.String_unsecure(),isOptional:!1},reason:{type:H.String_unsecure(),isOptional:!1},winningVariant:{type:H.String_unsecure(),isOptional:!0},stoppedBy:{type:H.String_unsecure(),isOptional:!0},stoppedAt:{type:H.DateTime(),isOptional:!1}}}),Lq=U({name:"FlagEvaluatedEventPayload",description:"Payload when a flag is evaluated (for analytics)",fields:{flagId:{type:H.String_unsecure(),isOptional:!1},flagKey:{type:H.String_unsecure(),isOptional:!1},subjectType:{type:H.String_unsecure(),isOptional:!1},subjectId:{type:H.String_unsecure(),isOptional:!1},result:{type:H.Boolean(),isOptional:!1},variant:{type:H.String_unsecure(),isOptional:!0},reason:{type:H.String_unsecure(),isOptional:!1},evaluatedAt:{type:H.DateTime(),isOptional:!1}}}),Uq=U({name:"VariantAssignedEventPayload",description:"Payload when a subject is assigned to an experiment variant",fields:{experimentId:{type:H.String_unsecure(),isOptional:!1},experimentKey:{type:H.String_unsecure(),isOptional:!1},subjectType:{type:H.String_unsecure(),isOptional:!1},subjectId:{type:H.String_unsecure(),isOptional:!1},variant:{type:H.String_unsecure(),isOptional:!1},bucket:{type:H.Int_unsecure(),isOptional:!1},assignedAt:{type:H.DateTime(),isOptional:!1}}}),Aq=L({meta:{key:"flag.created",version:"1.0.0",description:"A feature flag has been created.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","create"]},payload:Hq}),Qq=L({meta:{key:"flag.updated",version:"1.0.0",description:"A feature flag has been updated.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","update"]},payload:Jq}),Bq=L({meta:{key:"flag.deleted",version:"1.0.0",description:"A feature flag has been deleted.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","delete"]},payload:Kq}),Dq=L({meta:{key:"flag.toggled",version:"1.0.0",description:"A feature flag status has been toggled.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","toggle"]},payload:Xq}),Pq=L({meta:{key:"flag.rule_created",version:"1.0.0",description:"A targeting rule has been created.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","rule","create"]},payload:Yq}),Nq=L({meta:{key:"flag.rule_deleted",version:"1.0.0",description:"A targeting rule has been deleted.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","rule","delete"]},payload:Zq}),Wq=L({meta:{key:"experiment.created",version:"1.0.0",description:"An experiment has been created.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","experiment","create"]},payload:_q}),kq=L({meta:{key:"experiment.started",version:"1.0.0",description:"An experiment has started.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","experiment","start"]},payload:$q}),Cq=L({meta:{key:"experiment.stopped",version:"1.0.0",description:"An experiment has stopped.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","experiment","stop"]},payload:Gq}),Oq=L({meta:{key:"flag.evaluated",version:"1.0.0",description:"A feature flag has been evaluated.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","evaluate"]},payload:Lq}),Vq=L({meta:{key:"experiment.variant_assigned",version:"1.0.0",description:"A subject has been assigned to an experiment variant.",stability:"stable",owners:["@platform.feature-flags"],tags:["feature-flags","experiment","variant"]},payload:Uq}),lq={FlagCreatedEvent:Aq,FlagUpdatedEvent:Qq,FlagDeletedEvent:Bq,FlagToggledEvent:Dq,RuleCreatedEvent:Pq,RuleDeletedEvent:Nq,ExperimentCreatedEvent:Wq,ExperimentStartedEvent:kq,ExperimentStoppedEvent:Cq,FlagEvaluatedEvent:Oq,VariantAssignedEvent:Vq};import{defineFeature as jq}from"@contractspec/lib.contracts-spec/features";var qw=jq({meta:{key:"libs.feature-flags",version:"1.0.0",title:"Feature Flags",description:"Feature flags and experiments module for ContractSpec applications",domain:"feature-flags",owners:["@contractspec-core"],tags:["package","libs","feature-flags"],stability:"experimental"},operations:[{key:"flag.create",version:"1.0.0"},{key:"flag.update",version:"1.0.0"},{key:"flag.delete",version:"1.0.0"},{key:"flag.toggle",version:"1.0.0"},{key:"flag.get",version:"1.0.0"},{key:"flag.list",version:"1.0.0"},{key:"flag.evaluate",version:"1.0.0"},{key:"flag.rule.create",version:"1.0.0"},{key:"flag.rule.delete",version:"1.0.0"},{key:"experiment.create",version:"1.0.0"},{key:"experiment.start",version:"1.0.0"},{key:"experiment.stop",version:"1.0.0"},{key:"experiment.get",version:"1.0.0"}],events:[{key:"flag.created",version:"1.0.0"},{key:"flag.updated",version:"1.0.0"},{key:"flag.deleted",version:"1.0.0"},{key:"flag.toggled",version:"1.0.0"},{key:"flag.rule_created",version:"1.0.0"},{key:"flag.rule_deleted",version:"1.0.0"},{key:"experiment.created",version:"1.0.0"},{key:"experiment.started",version:"1.0.0"},{key:"experiment.stopped",version:"1.0.0"},{key:"flag.evaluated",version:"1.0.0"},{key:"experiment.variant_assigned",version:"1.0.0"}]});export{k as hashToBucket,C as getSubjectId,cq as featureFlagsSchemaContribution,a as featureFlagEntities,e as evaluateRuleCondition,Vq as VariantAssignedEvent,vq as UpdateFlagContract,bq as ToggleFlagContract,I as TargetingRuleModel,Sq as StopExperimentContract,yq as StartExperimentContract,F as RuleOperatorEnum,Nq as RuleDeletedEvent,Pq as RuleCreatedEvent,sq as ListFlagsContract,zq as InMemoryFlagRepository,gq as GetFlagContract,Eq as GetExperimentContract,Qq as FlagUpdatedEvent,Dq as FlagToggledEvent,u as FlagTargetingRuleEntity,R as FlagStatusEnum,wq as FlagEvaluator,l as FlagEvaluationEntity,Oq as FlagEvaluatedEvent,Bq as FlagDeletedEvent,Aq as FlagCreatedEvent,qw as FeatureFlagsFeature,D as FeatureFlagModel,lq as FeatureFlagEvents,r as FeatureFlagEntity,Cq as ExperimentStoppedEvent,M as ExperimentStatusEnum,kq as ExperimentStartedEvent,W as ExperimentModel,t as ExperimentEntity,Wq as ExperimentCreatedEvent,n as ExperimentAssignmentEntity,v as EvaluationResultModel,xq as EvaluateFlagContract,fq as DeleteRuleContract,hq as DeleteFlagContract,oq as CreateRuleContract,Iq as CreateFlagContract,Tq as CreateExperimentContract};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contractspec/lib.feature-flags",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.21",
|
|
4
4
|
"description": "Feature flags and experiments module for ContractSpec applications",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"contractspec",
|
|
@@ -27,13 +27,13 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@contractspec/lib.schema": "3.7.14",
|
|
30
|
-
"@contractspec/lib.contracts-spec": "5.
|
|
30
|
+
"@contractspec/lib.contracts-spec": "5.5.0",
|
|
31
31
|
"zod": "^4.3.5"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@contractspec/tool.typescript": "3.7.13",
|
|
35
35
|
"typescript": "^5.9.3",
|
|
36
|
-
"@contractspec/tool.bun": "3.7.
|
|
36
|
+
"@contractspec/tool.bun": "3.7.15"
|
|
37
37
|
},
|
|
38
38
|
"exports": {
|
|
39
39
|
".": {
|