@lssm/lib.feature-flags 0.0.0-canary-20251219202229 → 0.0.0-canary-20251220015515

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +1 -0
  2. package/dist/contracts/dist/capabilities/openbanking.js.map +1 -1
  3. package/dist/contracts/dist/contract-registry/schemas.js.map +1 -1
  4. package/dist/contracts/dist/docs/accessibility_wcag_compliance_specs.docblock.js.map +1 -1
  5. package/dist/contracts/dist/docs/meta.docs.js.map +1 -1
  6. package/dist/contracts/dist/docs/presentations.js.map +1 -1
  7. package/dist/contracts/dist/docs/registry.js.map +1 -1
  8. package/dist/contracts/dist/docs/tech/auth/better-auth-nextjs.docblock.js.map +1 -1
  9. package/dist/contracts/dist/docs/tech/contracts/openapi-export.docblock.js.map +1 -1
  10. package/dist/contracts/dist/docs/tech/lifecycle-stage-system.docblock.js.map +1 -1
  11. package/dist/contracts/dist/docs/tech/llm/llm-integration.docblock.js.map +1 -1
  12. package/dist/contracts/dist/docs/tech/mcp-endpoints.docblock.js.map +1 -1
  13. package/dist/contracts/dist/docs/tech/presentation-runtime.docblock.js.map +1 -1
  14. package/dist/contracts/dist/docs/tech/schema/README.docblock.js.map +1 -1
  15. package/dist/contracts/dist/docs/tech/studio/learning-events.docblock.js.map +1 -1
  16. package/dist/contracts/dist/docs/tech/studio/learning-journeys.docblock.js.map +1 -1
  17. package/dist/contracts/dist/docs/tech/studio/platform-admin-panel.docblock.js.map +1 -1
  18. package/dist/contracts/dist/docs/tech/studio/project-access-teams.docblock.js.map +1 -1
  19. package/dist/contracts/dist/docs/tech/studio/project-routing.docblock.js.map +1 -1
  20. package/dist/contracts/dist/docs/tech/studio/sandbox-unlogged.docblock.js.map +1 -1
  21. package/dist/contracts/dist/docs/tech/studio/team-invitations.docblock.js.map +1 -1
  22. package/dist/contracts/dist/docs/tech/studio/workspace-ops.docblock.js.map +1 -1
  23. package/dist/contracts/dist/docs/tech/studio/workspaces.docblock.js.map +1 -1
  24. package/dist/contracts/dist/docs/tech/telemetry-ingest.docblock.js.map +1 -1
  25. package/dist/contracts/dist/docs/tech/templates/runtime.docblock.js.map +1 -1
  26. package/dist/contracts/dist/docs/tech/vscode-extension.docblock.js.map +1 -1
  27. package/dist/contracts/dist/docs/tech/workflows/overview.docblock.js.map +1 -1
  28. package/dist/contracts/dist/docs/tech-contracts.docs.js.map +1 -1
  29. package/dist/contracts/dist/events.js.map +1 -1
  30. package/dist/contracts/dist/integrations/contracts.js.map +1 -1
  31. package/dist/contracts/dist/integrations/openbanking/contracts/accounts.js.map +1 -1
  32. package/dist/contracts/dist/integrations/openbanking/contracts/balances.js.map +1 -1
  33. package/dist/contracts/dist/integrations/openbanking/contracts/transactions.js.map +1 -1
  34. package/dist/contracts/dist/integrations/openbanking/models.js.map +1 -1
  35. package/dist/contracts/dist/integrations/openbanking/telemetry.js.map +1 -1
  36. package/dist/contracts/dist/integrations/providers/elevenlabs.js.map +1 -1
  37. package/dist/contracts/dist/integrations/providers/gcs-storage.js.map +1 -1
  38. package/dist/contracts/dist/integrations/providers/gmail.js.map +1 -1
  39. package/dist/contracts/dist/integrations/providers/google-calendar.js.map +1 -1
  40. package/dist/contracts/dist/integrations/providers/mistral.js.map +1 -1
  41. package/dist/contracts/dist/integrations/providers/postmark.js.map +1 -1
  42. package/dist/contracts/dist/integrations/providers/powens.js.map +1 -1
  43. package/dist/contracts/dist/integrations/providers/qdrant.js.map +1 -1
  44. package/dist/contracts/dist/integrations/providers/stripe.js.map +1 -1
  45. package/dist/contracts/dist/integrations/providers/twilio-sms.js.map +1 -1
  46. package/dist/contracts/dist/knowledge/contracts.js.map +1 -1
  47. package/dist/contracts/dist/knowledge/spaces/email-threads.js.map +1 -1
  48. package/dist/contracts/dist/knowledge/spaces/financial-docs.js.map +1 -1
  49. package/dist/contracts/dist/knowledge/spaces/financial-overview.js.map +1 -1
  50. package/dist/contracts/dist/knowledge/spaces/product-canon.js.map +1 -1
  51. package/dist/contracts/dist/knowledge/spaces/support-faq.js.map +1 -1
  52. package/dist/contracts/dist/knowledge/spaces/uploaded-docs.js.map +1 -1
  53. package/dist/contracts/dist/llm/exporters.js.map +1 -1
  54. package/dist/contracts/dist/onboarding-base.js.map +1 -1
  55. package/dist/contracts/dist/ownership.js +1 -0
  56. package/dist/contracts/dist/ownership.js.map +1 -1
  57. package/dist/contracts/dist/presentations.v2.js.map +1 -1
  58. package/dist/contracts/dist/regenerator/service.js.map +1 -1
  59. package/dist/contracts/dist/schema/dist/index.js.map +1 -1
  60. package/dist/contracts/dist/spec.js.map +1 -1
  61. package/dist/evaluation/index.d.ts.map +1 -1
  62. package/dist/evaluation/index.js.map +1 -1
  63. package/package.json +13 -7
@@ -1 +1 @@
1
- {"version":3,"file":"spec.js","names":[],"sources":["../../../../contracts/dist/spec.js"],"sourcesContent":["//#region src/spec.ts\nconst isEmitDeclRef = (e) => \"ref\" in e;\n/**\n* Helper to define a Command (write operation).\n* Sets `kind: 'command'` and defaults `idempotent: false`.\n*/\nconst defineCommand = (spec) => ({\n\t...spec,\n\tmeta: {\n\t\t...spec.meta,\n\t\tkind: \"command\"\n\t},\n\tpolicy: {\n\t\t...spec.policy,\n\t\tidempotent: spec.policy?.[\"policy\"]?.idempotent ?? false\n\t}\n});\n/**\n* Helper to define a Query (read-only operation).\n* Sets `kind: 'query'` and forces `idempotent: true`.\n*/\nconst defineQuery = (spec) => ({\n\t...spec,\n\tmeta: {\n\t\t...spec.meta,\n\t\tkind: \"query\"\n\t},\n\tpolicy: {\n\t\t...spec.policy,\n\t\tidempotent: true\n\t}\n});\n\n//#endregion\nexport { defineCommand, defineQuery, isEmitDeclRef };\n//# sourceMappingURL=spec.js.map"],"mappings":";;;;;AAMA,MAAM,iBAAiB,UAAU;CAChC,GAAG;CACH,MAAM;EACL,GAAG,KAAK;EACR,MAAM;EACN;CACD,QAAQ;EACP,GAAG,KAAK;EACR,YAAY,KAAK,SAAS,WAAW,cAAc;EACnD;CACD;;;;;AAKD,MAAM,eAAe,UAAU;CAC9B,GAAG;CACH,MAAM;EACL,GAAG,KAAK;EACR,MAAM;EACN;CACD,QAAQ;EACP,GAAG,KAAK;EACR,YAAY;EACZ;CACD"}
1
+ {"version":3,"file":"spec.js","names":[],"sources":["../../../../contracts/dist/spec.js"],"sourcesContent":["//#region src/spec.ts\nconst isEmitDeclRef = (e) => \"ref\" in e;\n/**\n* Helper to define a Command (write operation).\n* Sets `kind: 'command'` and defaults `idempotent: false`.\n*/\nconst defineCommand = (spec) => ({\n\t...spec,\n\tmeta: {\n\t\t...spec.meta,\n\t\tkind: \"command\"\n\t},\n\tpolicy: {\n\t\t...spec.policy,\n\t\tidempotent: spec.policy?.[\"policy\"]?.idempotent ?? false\n\t}\n});\n/**\n* Helper to define a Query (read-only operation).\n* Sets `kind: 'query'` and forces `idempotent: true`.\n*/\nconst defineQuery = (spec) => ({\n\t...spec,\n\tmeta: {\n\t\t...spec.meta,\n\t\tkind: \"query\"\n\t},\n\tpolicy: {\n\t\t...spec.policy,\n\t\tidempotent: true\n\t}\n});\n\n//#endregion\nexport { defineCommand, defineQuery, isEmitDeclRef };"],"mappings":";;;;;AAMA,MAAM,iBAAiB,UAAU;CAChC,GAAG;CACH,MAAM;EACL,GAAG,KAAK;EACR,MAAM;EACN;CACD,QAAQ;EACP,GAAG,KAAK;EACR,YAAY,KAAK,SAAS,WAAW,cAAc;EACnD;CACD;;;;;AAKD,MAAM,eAAe,UAAU;CAC9B,GAAG;CACH,MAAM;EACL,GAAG,KAAK;EACR,MAAM;EACN;CACD,QAAQ;EACP,GAAG,KAAK;EACR,YAAY;EACZ;CACD"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/evaluation/index.ts"],"sourcesContent":[],"mappings":";;AASA;AAeA;AAQA;AAYA;AASA;AAOiB,UAnDA,iBAAA,CAmDiB;EAMjB;EAQL,MAAA,CAAA,EAAA,MAAA;EASA;EAmBI,KAAA,CAAA,EAAA,MAAA;EAeA;EASA,IAAA,CAAA,EAAA,MAAA;EAuFC;EAC+B,OAAA,CAAA,EAAA,MAAA;EAAR;EACJ,SAAA,CAAA,EAAA,MAAA;EAAR;EACmB,UAAA,CAAA,EAnMhC,MAmMgC,CAAA,MAAA,EAAA,OAAA,CAAA;;AAK1C,UArMY,WAAA,CAqMZ;EAOA,EAAA,EAAA,MAAA;EAAO,GAAA,EAAA,MAAA;EAGK,MAAA,EAAA,KAAA,GAAA,IAAgB,GAAA,SAWnB;EAIG,YAAA,EAAA,OAAA;EAgBJ,QAAA,CAAA,EAzOA,aAyOa,EAAA;;AAgBb,UAtPI,aAAA,CAsPJ;EACA,EAAA,EAAA,MAAA;EAAR,QAAA,EAAA,MAAA;EAAO,OAAA,EAAA,OAAA;EAsMC,SAAA,EAAA,MAAA;EAMG,QAAA,EA9bJ,YA8bI;EAIgB,KAAA,EAAA,OAAA;EAMJ,iBAAA,CAAA,EAAA,MAAA;EAIU,UAAA,CAAA,EAAA,OAAA;EAAR,YAAA,CAAA,EAAA,MAAA;;AAII,UAzcjB,UAAA,CAyciB;EAImB,EAAA,EAAA,MAAA;EAAR,GAAA,EAAA,MAAA;EAQxC,MAAA,EAAA,OAAA,GAAA,SAAA,GAAA,QAAA,GAAA,WAAA,GAAA,WAAA;EAUA,QAAA,EA3dO,iBA2dP,EAAA;EA9C0C,kBAAA,EAAA,MAAA;EAAc,cAAA,CAAA,EA3a1C,MA2a0C,CAAA,MAAA,EAAA,OAAA,CAAA;;UAxa5C,aAAA;;;;;;UAOA,iBAAA;;;;;UAMA,gBAAA;;;UAGP;;;;KAKE,gBAAA;KASA,YAAA;;;;;iBAmBI,YAAA;;;;iBAeA,YAAA,UAAsB;;;;iBAStB,qBAAA,OACR,wBACG;UAqFM,cAAA;wCACuB,QAAQ;4BACpB,QAAQ;uCACG,QAAQ;yFAK1C;2HAOA;;UAGY,gBAAA;;;;;;;;;;;cAWH;;;UAIG,oBAAA;cACH;WACH;;;;;;;;;;;;;cAcE,aAAA;;;;uBAKU;;;;iCAWV,oBACR,QAAQ;;;;;;;;;;;;;;;;;;;;;cAsMA,sBAAA,YAAkC;;;;;gBAM/B;gCAIgB;4BAMJ;wBAIE,QAAQ;4BAIJ,QAAQ;uCAIG,QAAQ;yFAQhD;2GAUA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/evaluation/index.ts"],"sourcesContent":[],"mappings":";;AASA;AAeA;AAQA;AAYA;AASA;AAOiB,UAnDA,iBAAA,CAmDiB;EAMjB;EAQL,MAAA,CAAA,EAAA,MAAA;EASA;EAmBI,KAAA,CAAA,EAAA,MAAA;EAeA;EASA,IAAA,CAAA,EAAA,MAAA;EAwFC;EAC+B,OAAA,CAAA,EAAA,MAAA;EAAR;EACJ,SAAA,CAAA,EAAA,MAAA;EAAR;EACmB,UAAA,CAAA,EApMhC,MAoMgC,CAAA,MAAA,EAAA,OAAA,CAAA;;AAK1C,UAtMY,WAAA,CAsMZ;EAOA,EAAA,EAAA,MAAA;EAAO,GAAA,EAAA,MAAA;EAGK,MAAA,EAAA,KAAA,GAAA,IAAgB,GAAA,SAWnB;EAIG,YAAA,EAAA,OAAA;EAgBJ,QAAA,CAAA,EA1OA,aA0Oa,EAAA;;AAgBb,UAvPI,aAAA,CAuPJ;EACA,EAAA,EAAA,MAAA;EAAR,QAAA,EAAA,MAAA;EAAO,OAAA,EAAA,OAAA;EAsMC,SAAA,EAAA,MAAA;EAMG,QAAA,EA/bJ,YA+bI;EAIgB,KAAA,EAAA,OAAA;EAMJ,iBAAA,CAAA,EAAA,MAAA;EAIU,UAAA,CAAA,EAAA,OAAA;EAAR,YAAA,CAAA,EAAA,MAAA;;AAII,UA1cjB,UAAA,CA0ciB;EAImB,EAAA,EAAA,MAAA;EAAR,GAAA,EAAA,MAAA;EAQxC,MAAA,EAAA,OAAA,GAAA,SAAA,GAAA,QAAA,GAAA,WAAA,GAAA,WAAA;EAUA,QAAA,EA5dO,iBA4dP,EAAA;EA9C0C,kBAAA,EAAA,MAAA;EAAc,cAAA,CAAA,EA5a1C,MA4a0C,CAAA,MAAA,EAAA,OAAA,CAAA;;UAza5C,aAAA;;;;;;UAOA,iBAAA;;;;;UAMA,gBAAA;;;UAGP;;;;KAKE,gBAAA;KASA,YAAA;;;;;iBAmBI,YAAA;;;;iBAeA,YAAA,UAAsB;;;;iBAStB,qBAAA,OACR,wBACG;UAsFM,cAAA;wCACuB,QAAQ;4BACpB,QAAQ;uCACG,QAAQ;yFAK1C;2HAOA;;UAGY,gBAAA;;;;;;;;;;;cAWH;;;UAIG,oBAAA;cACH;WACH;;;;;;;;;;;;;cAcE,aAAA;;;;uBAKU;;;;iCAWV,oBACR,QAAQ;;;;;;;;;;;;;;;;;;;;;cAsMA,sBAAA,YAAkC;;;;;gBAM/B;gCAIgB;4BAMJ;wBAIE,QAAQ;4BAIJ,QAAQ;uCAIG,QAAQ;yFAQhD;2GAUA"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/evaluation/index.ts"],"sourcesContent":["/**\n * Feature flag evaluation engine.\n *\n * Provides deterministic evaluation of feature flags based on targeting rules\n * and experiment assignments.\n */\n\n// ============ Types ============\n\nexport interface EvaluationContext {\n /** User identifier */\n userId?: string;\n /** Organization identifier */\n orgId?: string;\n /** User's plan (free, pro, enterprise, etc.) */\n plan?: string;\n /** User segment or cohort */\n segment?: string;\n /** Session identifier for anonymous users */\n sessionId?: string;\n /** Additional custom attributes */\n attributes?: Record<string, unknown>;\n}\n\nexport interface FeatureFlag {\n id: string;\n key: string;\n status: 'OFF' | 'ON' | 'GRADUAL';\n defaultValue: boolean;\n variants?: VariantConfig[];\n}\n\nexport interface TargetingRule {\n id: string;\n priority: number;\n enabled: boolean;\n attribute: string;\n operator: RuleOperator;\n value: unknown;\n rolloutPercentage?: number;\n serveValue?: boolean;\n serveVariant?: string;\n}\n\nexport interface Experiment {\n id: string;\n key: string;\n status: 'DRAFT' | 'RUNNING' | 'PAUSED' | 'COMPLETED' | 'CANCELLED';\n variants: ExperimentVariant[];\n audiencePercentage: number;\n audienceFilter?: Record<string, unknown>;\n}\n\nexport interface VariantConfig {\n key: string;\n name: string;\n description?: string;\n weight?: number;\n}\n\nexport interface ExperimentVariant {\n key: string;\n name: string;\n percentage: number;\n}\n\nexport interface EvaluationResult {\n enabled: boolean;\n variant?: string;\n reason: EvaluationReason;\n ruleId?: string;\n experimentId?: string;\n}\n\nexport type EvaluationReason =\n | 'FLAG_OFF'\n | 'FLAG_ON'\n | 'DEFAULT_VALUE'\n | 'RULE_MATCH'\n | 'PERCENTAGE_ROLLOUT'\n | 'EXPERIMENT_VARIANT'\n | 'FLAG_NOT_FOUND';\n\nexport type RuleOperator =\n | 'EQ'\n | 'NEQ'\n | 'IN'\n | 'NIN'\n | 'CONTAINS'\n | 'NOT_CONTAINS'\n | 'GT'\n | 'GTE'\n | 'LT'\n | 'LTE'\n | 'PERCENTAGE';\n\n// ============ Hashing ============\n\n/**\n * Simple hash function for consistent bucketing.\n * Uses a deterministic algorithm so the same input always produces the same bucket.\n */\nexport function hashToBucket(value: string, seed = ''): number {\n const input = `${seed}:${value}`;\n let hash = 0;\n for (let i = 0; i < input.length; i++) {\n const char = input.charCodeAt(i);\n hash = (hash << 5) - hash + char;\n hash = hash & hash; // Convert to 32-bit integer\n }\n // Return a number between 0 and 99\n return Math.abs(hash % 100);\n}\n\n/**\n * Get subject identifier from context for consistent hashing.\n */\nexport function getSubjectId(context: EvaluationContext): string {\n return context.userId || context.sessionId || context.orgId || 'anonymous';\n}\n\n// ============ Rule Evaluation ============\n\n/**\n * Evaluate a single targeting rule condition.\n */\nexport function evaluateRuleCondition(\n rule: TargetingRule,\n context: EvaluationContext\n): boolean {\n const attributeValue = getAttributeValue(rule.attribute, context);\n\n switch (rule.operator) {\n case 'EQ':\n return attributeValue === rule.value;\n\n case 'NEQ':\n return attributeValue !== rule.value;\n\n case 'IN':\n if (!Array.isArray(rule.value)) return false;\n return rule.value.includes(attributeValue);\n\n case 'NIN':\n if (!Array.isArray(rule.value)) return true;\n return !rule.value.includes(attributeValue);\n\n case 'CONTAINS':\n if (typeof attributeValue !== 'string' || typeof rule.value !== 'string')\n return false;\n return attributeValue.includes(rule.value);\n\n case 'NOT_CONTAINS':\n if (typeof attributeValue !== 'string' || typeof rule.value !== 'string')\n return true;\n return !attributeValue.includes(rule.value);\n\n case 'GT':\n if (typeof attributeValue !== 'number' || typeof rule.value !== 'number')\n return false;\n return attributeValue > rule.value;\n\n case 'GTE':\n if (typeof attributeValue !== 'number' || typeof rule.value !== 'number')\n return false;\n return attributeValue >= rule.value;\n\n case 'LT':\n if (typeof attributeValue !== 'number' || typeof rule.value !== 'number')\n return false;\n return attributeValue < rule.value;\n\n case 'LTE':\n if (typeof attributeValue !== 'number' || typeof rule.value !== 'number')\n return false;\n return attributeValue <= rule.value;\n\n case 'PERCENTAGE':\n // Percentage-based targeting uses consistent hashing\n const bucket = hashToBucket(getSubjectId(context), rule.attribute);\n const percentage = typeof rule.value === 'number' ? rule.value : 0;\n return bucket < percentage;\n\n default:\n return false;\n }\n}\n\n/**\n * Get attribute value from context.\n */\nfunction getAttributeValue(\n attribute: string,\n context: EvaluationContext\n): unknown {\n switch (attribute) {\n case 'userId':\n return context.userId;\n case 'orgId':\n return context.orgId;\n case 'plan':\n return context.plan;\n case 'segment':\n return context.segment;\n case 'sessionId':\n return context.sessionId;\n default:\n return context.attributes?.[attribute];\n }\n}\n\n// ============ Flag Evaluator ============\n\nexport interface FlagRepository {\n getFlag(key: string, orgId?: string): Promise<FeatureFlag | null>;\n getRules(flagId: string): Promise<TargetingRule[]>;\n getActiveExperiment(flagId: string): Promise<Experiment | null>;\n getExperimentAssignment(\n experimentId: string,\n subjectType: string,\n subjectId: string\n ): Promise<string | null>;\n saveExperimentAssignment(\n experimentId: string,\n subjectType: string,\n subjectId: string,\n variant: string,\n bucket: number\n ): Promise<void>;\n}\n\nexport interface EvaluationLogger {\n log(evaluation: {\n flagId: string;\n flagKey: string;\n subjectType: string;\n subjectId: string;\n result: boolean;\n variant?: string;\n reason: string;\n ruleId?: string;\n experimentId?: string;\n context?: EvaluationContext;\n }): void;\n}\n\nexport interface FlagEvaluatorOptions {\n repository: FlagRepository;\n logger?: EvaluationLogger;\n /** Whether to log evaluations (default: false for performance) */\n logEvaluations?: boolean;\n}\n\n/**\n * Feature flag evaluator.\n *\n * Evaluates flags based on:\n * 1. Flag status (OFF/ON/GRADUAL)\n * 2. Targeting rules (in priority order)\n * 3. Experiments (if running)\n * 4. Default value (fallback)\n */\nexport class FlagEvaluator {\n private repository: FlagRepository;\n private logger?: EvaluationLogger;\n private logEvaluations: boolean;\n\n constructor(options: FlagEvaluatorOptions) {\n this.repository = options.repository;\n this.logger = options.logger;\n this.logEvaluations = options.logEvaluations ?? false;\n }\n\n /**\n * Evaluate a feature flag.\n */\n async evaluate(\n key: string,\n context: EvaluationContext\n ): Promise<EvaluationResult> {\n const orgId = context.orgId;\n const flag = await this.repository.getFlag(key, orgId);\n\n if (!flag) {\n return this.makeResult(false, 'FLAG_NOT_FOUND');\n }\n\n // Check flag status\n if (flag.status === 'OFF') {\n return this.logAndReturn(\n flag,\n context,\n this.makeResult(false, 'FLAG_OFF')\n );\n }\n\n if (flag.status === 'ON') {\n return this.logAndReturn(flag, context, this.makeResult(true, 'FLAG_ON'));\n }\n\n // Status is GRADUAL - evaluate rules and experiments\n const rules = await this.repository.getRules(flag.id);\n\n // Sort rules by priority (lower = higher priority)\n const sortedRules = [...rules]\n .filter((r) => r.enabled)\n .sort((a, b) => a.priority - b.priority);\n\n // Evaluate rules in order\n for (const rule of sortedRules) {\n if (evaluateRuleCondition(rule, context)) {\n // Rule matched - check for percentage rollout\n if (\n rule.rolloutPercentage !== undefined &&\n rule.rolloutPercentage !== null\n ) {\n const bucket = hashToBucket(getSubjectId(context), flag.key);\n if (bucket >= rule.rolloutPercentage) {\n continue; // User not in rollout percentage, try next rule\n }\n }\n\n const enabled = rule.serveValue ?? true;\n return this.logAndReturn(\n flag,\n context,\n this.makeResult(enabled, 'RULE_MATCH', rule.serveVariant, rule.id)\n );\n }\n }\n\n // Check for active experiment\n const experiment = await this.repository.getActiveExperiment(flag.id);\n if (experiment && experiment.status === 'RUNNING') {\n const result = await this.evaluateExperiment(experiment, context);\n if (result) {\n return this.logAndReturn(flag, context, result);\n }\n }\n\n // Fall back to default value\n return this.logAndReturn(\n flag,\n context,\n this.makeResult(flag.defaultValue, 'DEFAULT_VALUE')\n );\n }\n\n /**\n * Evaluate experiment and assign variant.\n */\n private async evaluateExperiment(\n experiment: Experiment,\n context: EvaluationContext\n ): Promise<EvaluationResult | null> {\n const subjectId = getSubjectId(context);\n const subjectType = context.userId\n ? 'user'\n : context.orgId\n ? 'org'\n : 'session';\n\n // Check audience percentage\n const audienceBucket = hashToBucket(\n subjectId,\n `${experiment.key}:audience`\n );\n if (audienceBucket >= experiment.audiencePercentage) {\n return null; // User not in experiment audience\n }\n\n // Check for existing assignment\n let variant = await this.repository.getExperimentAssignment(\n experiment.id,\n subjectType,\n subjectId\n );\n\n if (!variant) {\n // Assign to variant based on consistent hashing\n const variantBucket = hashToBucket(subjectId, experiment.key);\n variant = this.assignVariant(experiment.variants, variantBucket);\n\n // Save assignment\n await this.repository.saveExperimentAssignment(\n experiment.id,\n subjectType,\n subjectId,\n variant,\n variantBucket\n );\n }\n\n // Control variant typically means feature is off\n const enabled = variant !== 'control';\n\n return this.makeResult(\n enabled,\n 'EXPERIMENT_VARIANT',\n variant,\n undefined,\n experiment.id\n );\n }\n\n /**\n * Assign a variant based on bucket and variant percentages.\n */\n private assignVariant(variants: ExperimentVariant[], bucket: number): string {\n let cumulative = 0;\n for (const variant of variants) {\n cumulative += variant.percentage;\n if (bucket < cumulative) {\n return variant.key;\n }\n }\n // Fallback to last variant (shouldn't happen if percentages sum to 100)\n return variants[variants.length - 1]?.key ?? 'control';\n }\n\n /**\n * Create evaluation result.\n */\n private makeResult(\n enabled: boolean,\n reason: EvaluationReason,\n variant?: string,\n ruleId?: string,\n experimentId?: string\n ): EvaluationResult {\n return {\n enabled,\n variant,\n reason,\n ruleId,\n experimentId,\n };\n }\n\n /**\n * Log evaluation and return result.\n */\n private logAndReturn(\n flag: FeatureFlag,\n context: EvaluationContext,\n result: EvaluationResult\n ): EvaluationResult {\n if (this.logEvaluations && this.logger) {\n const subjectId = getSubjectId(context);\n const subjectType = context.userId\n ? 'user'\n : context.orgId\n ? 'org'\n : 'session';\n\n this.logger.log({\n flagId: flag.id,\n flagKey: flag.key,\n subjectType,\n subjectId,\n result: result.enabled,\n variant: result.variant,\n reason: result.reason,\n ruleId: result.ruleId,\n experimentId: result.experimentId,\n context,\n });\n }\n return result;\n }\n}\n\n// ============ In-Memory Repository ============\n\n/**\n * In-memory flag repository for testing and development.\n */\nexport class InMemoryFlagRepository implements FlagRepository {\n private flags = new Map<string, FeatureFlag>();\n private rules = new Map<string, TargetingRule[]>();\n private experiments = new Map<string, Experiment>();\n private assignments = new Map<string, string>();\n\n addFlag(flag: FeatureFlag): void {\n this.flags.set(flag.key, flag);\n }\n\n addRule(flagId: string, rule: TargetingRule): void {\n const existing = this.rules.get(flagId) || [];\n existing.push(rule);\n this.rules.set(flagId, existing);\n }\n\n addExperiment(experiment: Experiment, flagId: string): void {\n this.experiments.set(flagId, experiment);\n }\n\n async getFlag(key: string): Promise<FeatureFlag | null> {\n return this.flags.get(key) || null;\n }\n\n async getRules(flagId: string): Promise<TargetingRule[]> {\n return this.rules.get(flagId) || [];\n }\n\n async getActiveExperiment(flagId: string): Promise<Experiment | null> {\n return this.experiments.get(flagId) || null;\n }\n\n async getExperimentAssignment(\n experimentId: string,\n subjectType: string,\n subjectId: string\n ): Promise<string | null> {\n const key = `${experimentId}:${subjectType}:${subjectId}`;\n return this.assignments.get(key) || null;\n }\n\n async saveExperimentAssignment(\n experimentId: string,\n subjectType: string,\n subjectId: string,\n variant: string\n ): Promise<void> {\n const key = `${experimentId}:${subjectType}:${subjectId}`;\n this.assignments.set(key, variant);\n }\n\n clear(): void {\n this.flags.clear();\n this.rules.clear();\n this.experiments.clear();\n this.assignments.clear();\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/evaluation/index.ts"],"sourcesContent":["/**\n * Feature flag evaluation engine.\n *\n * Provides deterministic evaluation of feature flags based on targeting rules\n * and experiment assignments.\n */\n\n// ============ Types ============\n\nexport interface EvaluationContext {\n /** User identifier */\n userId?: string;\n /** Organization identifier */\n orgId?: string;\n /** User's plan (free, pro, enterprise, etc.) */\n plan?: string;\n /** User segment or cohort */\n segment?: string;\n /** Session identifier for anonymous users */\n sessionId?: string;\n /** Additional custom attributes */\n attributes?: Record<string, unknown>;\n}\n\nexport interface FeatureFlag {\n id: string;\n key: string;\n status: 'OFF' | 'ON' | 'GRADUAL';\n defaultValue: boolean;\n variants?: VariantConfig[];\n}\n\nexport interface TargetingRule {\n id: string;\n priority: number;\n enabled: boolean;\n attribute: string;\n operator: RuleOperator;\n value: unknown;\n rolloutPercentage?: number;\n serveValue?: boolean;\n serveVariant?: string;\n}\n\nexport interface Experiment {\n id: string;\n key: string;\n status: 'DRAFT' | 'RUNNING' | 'PAUSED' | 'COMPLETED' | 'CANCELLED';\n variants: ExperimentVariant[];\n audiencePercentage: number;\n audienceFilter?: Record<string, unknown>;\n}\n\nexport interface VariantConfig {\n key: string;\n name: string;\n description?: string;\n weight?: number;\n}\n\nexport interface ExperimentVariant {\n key: string;\n name: string;\n percentage: number;\n}\n\nexport interface EvaluationResult {\n enabled: boolean;\n variant?: string;\n reason: EvaluationReason;\n ruleId?: string;\n experimentId?: string;\n}\n\nexport type EvaluationReason =\n | 'FLAG_OFF'\n | 'FLAG_ON'\n | 'DEFAULT_VALUE'\n | 'RULE_MATCH'\n | 'PERCENTAGE_ROLLOUT'\n | 'EXPERIMENT_VARIANT'\n | 'FLAG_NOT_FOUND';\n\nexport type RuleOperator =\n | 'EQ'\n | 'NEQ'\n | 'IN'\n | 'NIN'\n | 'CONTAINS'\n | 'NOT_CONTAINS'\n | 'GT'\n | 'GTE'\n | 'LT'\n | 'LTE'\n | 'PERCENTAGE';\n\n// ============ Hashing ============\n\n/**\n * Simple hash function for consistent bucketing.\n * Uses a deterministic algorithm so the same input always produces the same bucket.\n */\nexport function hashToBucket(value: string, seed = ''): number {\n const input = `${seed}:${value}`;\n let hash = 0;\n for (let i = 0; i < input.length; i++) {\n const char = input.charCodeAt(i);\n hash = (hash << 5) - hash + char;\n hash = hash & hash; // Convert to 32-bit integer\n }\n // Return a number between 0 and 99\n return Math.abs(hash % 100);\n}\n\n/**\n * Get subject identifier from context for consistent hashing.\n */\nexport function getSubjectId(context: EvaluationContext): string {\n return context.userId || context.sessionId || context.orgId || 'anonymous';\n}\n\n// ============ Rule Evaluation ============\n\n/**\n * Evaluate a single targeting rule condition.\n */\nexport function evaluateRuleCondition(\n rule: TargetingRule,\n context: EvaluationContext\n): boolean {\n const attributeValue = getAttributeValue(rule.attribute, context);\n\n switch (rule.operator) {\n case 'EQ':\n return attributeValue === rule.value;\n\n case 'NEQ':\n return attributeValue !== rule.value;\n\n case 'IN':\n if (!Array.isArray(rule.value)) return false;\n return rule.value.includes(attributeValue);\n\n case 'NIN':\n if (!Array.isArray(rule.value)) return true;\n return !rule.value.includes(attributeValue);\n\n case 'CONTAINS':\n if (typeof attributeValue !== 'string' || typeof rule.value !== 'string')\n return false;\n return attributeValue.includes(rule.value);\n\n case 'NOT_CONTAINS':\n if (typeof attributeValue !== 'string' || typeof rule.value !== 'string')\n return true;\n return !attributeValue.includes(rule.value);\n\n case 'GT':\n if (typeof attributeValue !== 'number' || typeof rule.value !== 'number')\n return false;\n return attributeValue > rule.value;\n\n case 'GTE':\n if (typeof attributeValue !== 'number' || typeof rule.value !== 'number')\n return false;\n return attributeValue >= rule.value;\n\n case 'LT':\n if (typeof attributeValue !== 'number' || typeof rule.value !== 'number')\n return false;\n return attributeValue < rule.value;\n\n case 'LTE':\n if (typeof attributeValue !== 'number' || typeof rule.value !== 'number')\n return false;\n return attributeValue <= rule.value;\n\n case 'PERCENTAGE':\n // Percentage-based targeting uses consistent hashing\n return (\n hashToBucket(getSubjectId(context), rule.attribute) <\n (typeof rule.value === 'number' ? rule.value : 0)\n );\n\n default:\n return false;\n }\n}\n\n/**\n * Get attribute value from context.\n */\nfunction getAttributeValue(\n attribute: string,\n context: EvaluationContext\n): unknown {\n switch (attribute) {\n case 'userId':\n return context.userId;\n case 'orgId':\n return context.orgId;\n case 'plan':\n return context.plan;\n case 'segment':\n return context.segment;\n case 'sessionId':\n return context.sessionId;\n default:\n return context.attributes?.[attribute];\n }\n}\n\n// ============ Flag Evaluator ============\n\nexport interface FlagRepository {\n getFlag(key: string, orgId?: string): Promise<FeatureFlag | null>;\n getRules(flagId: string): Promise<TargetingRule[]>;\n getActiveExperiment(flagId: string): Promise<Experiment | null>;\n getExperimentAssignment(\n experimentId: string,\n subjectType: string,\n subjectId: string\n ): Promise<string | null>;\n saveExperimentAssignment(\n experimentId: string,\n subjectType: string,\n subjectId: string,\n variant: string,\n bucket: number\n ): Promise<void>;\n}\n\nexport interface EvaluationLogger {\n log(evaluation: {\n flagId: string;\n flagKey: string;\n subjectType: string;\n subjectId: string;\n result: boolean;\n variant?: string;\n reason: string;\n ruleId?: string;\n experimentId?: string;\n context?: EvaluationContext;\n }): void;\n}\n\nexport interface FlagEvaluatorOptions {\n repository: FlagRepository;\n logger?: EvaluationLogger;\n /** Whether to log evaluations (default: false for performance) */\n logEvaluations?: boolean;\n}\n\n/**\n * Feature flag evaluator.\n *\n * Evaluates flags based on:\n * 1. Flag status (OFF/ON/GRADUAL)\n * 2. Targeting rules (in priority order)\n * 3. Experiments (if running)\n * 4. Default value (fallback)\n */\nexport class FlagEvaluator {\n private repository: FlagRepository;\n private logger?: EvaluationLogger;\n private logEvaluations: boolean;\n\n constructor(options: FlagEvaluatorOptions) {\n this.repository = options.repository;\n this.logger = options.logger;\n this.logEvaluations = options.logEvaluations ?? false;\n }\n\n /**\n * Evaluate a feature flag.\n */\n async evaluate(\n key: string,\n context: EvaluationContext\n ): Promise<EvaluationResult> {\n const orgId = context.orgId;\n const flag = await this.repository.getFlag(key, orgId);\n\n if (!flag) {\n return this.makeResult(false, 'FLAG_NOT_FOUND');\n }\n\n // Check flag status\n if (flag.status === 'OFF') {\n return this.logAndReturn(\n flag,\n context,\n this.makeResult(false, 'FLAG_OFF')\n );\n }\n\n if (flag.status === 'ON') {\n return this.logAndReturn(flag, context, this.makeResult(true, 'FLAG_ON'));\n }\n\n // Status is GRADUAL - evaluate rules and experiments\n const rules = await this.repository.getRules(flag.id);\n\n // Sort rules by priority (lower = higher priority)\n const sortedRules = [...rules]\n .filter((r) => r.enabled)\n .sort((a, b) => a.priority - b.priority);\n\n // Evaluate rules in order\n for (const rule of sortedRules) {\n if (evaluateRuleCondition(rule, context)) {\n // Rule matched - check for percentage rollout\n if (\n rule.rolloutPercentage !== undefined &&\n rule.rolloutPercentage !== null\n ) {\n const bucket = hashToBucket(getSubjectId(context), flag.key);\n if (bucket >= rule.rolloutPercentage) {\n continue; // User not in rollout percentage, try next rule\n }\n }\n\n const enabled = rule.serveValue ?? true;\n return this.logAndReturn(\n flag,\n context,\n this.makeResult(enabled, 'RULE_MATCH', rule.serveVariant, rule.id)\n );\n }\n }\n\n // Check for active experiment\n const experiment = await this.repository.getActiveExperiment(flag.id);\n if (experiment && experiment.status === 'RUNNING') {\n const result = await this.evaluateExperiment(experiment, context);\n if (result) {\n return this.logAndReturn(flag, context, result);\n }\n }\n\n // Fall back to default value\n return this.logAndReturn(\n flag,\n context,\n this.makeResult(flag.defaultValue, 'DEFAULT_VALUE')\n );\n }\n\n /**\n * Evaluate experiment and assign variant.\n */\n private async evaluateExperiment(\n experiment: Experiment,\n context: EvaluationContext\n ): Promise<EvaluationResult | null> {\n const subjectId = getSubjectId(context);\n const subjectType = context.userId\n ? 'user'\n : context.orgId\n ? 'org'\n : 'session';\n\n // Check audience percentage\n const audienceBucket = hashToBucket(\n subjectId,\n `${experiment.key}:audience`\n );\n if (audienceBucket >= experiment.audiencePercentage) {\n return null; // User not in experiment audience\n }\n\n // Check for existing assignment\n let variant = await this.repository.getExperimentAssignment(\n experiment.id,\n subjectType,\n subjectId\n );\n\n if (!variant) {\n // Assign to variant based on consistent hashing\n const variantBucket = hashToBucket(subjectId, experiment.key);\n variant = this.assignVariant(experiment.variants, variantBucket);\n\n // Save assignment\n await this.repository.saveExperimentAssignment(\n experiment.id,\n subjectType,\n subjectId,\n variant,\n variantBucket\n );\n }\n\n // Control variant typically means feature is off\n const enabled = variant !== 'control';\n\n return this.makeResult(\n enabled,\n 'EXPERIMENT_VARIANT',\n variant,\n undefined,\n experiment.id\n );\n }\n\n /**\n * Assign a variant based on bucket and variant percentages.\n */\n private assignVariant(variants: ExperimentVariant[], bucket: number): string {\n let cumulative = 0;\n for (const variant of variants) {\n cumulative += variant.percentage;\n if (bucket < cumulative) {\n return variant.key;\n }\n }\n // Fallback to last variant (shouldn't happen if percentages sum to 100)\n return variants[variants.length - 1]?.key ?? 'control';\n }\n\n /**\n * Create evaluation result.\n */\n private makeResult(\n enabled: boolean,\n reason: EvaluationReason,\n variant?: string,\n ruleId?: string,\n experimentId?: string\n ): EvaluationResult {\n return {\n enabled,\n variant,\n reason,\n ruleId,\n experimentId,\n };\n }\n\n /**\n * Log evaluation and return result.\n */\n private logAndReturn(\n flag: FeatureFlag,\n context: EvaluationContext,\n result: EvaluationResult\n ): EvaluationResult {\n if (this.logEvaluations && this.logger) {\n const subjectId = getSubjectId(context);\n const subjectType = context.userId\n ? 'user'\n : context.orgId\n ? 'org'\n : 'session';\n\n this.logger.log({\n flagId: flag.id,\n flagKey: flag.key,\n subjectType,\n subjectId,\n result: result.enabled,\n variant: result.variant,\n reason: result.reason,\n ruleId: result.ruleId,\n experimentId: result.experimentId,\n context,\n });\n }\n return result;\n }\n}\n\n// ============ In-Memory Repository ============\n\n/**\n * In-memory flag repository for testing and development.\n */\nexport class InMemoryFlagRepository implements FlagRepository {\n private flags = new Map<string, FeatureFlag>();\n private rules = new Map<string, TargetingRule[]>();\n private experiments = new Map<string, Experiment>();\n private assignments = new Map<string, string>();\n\n addFlag(flag: FeatureFlag): void {\n this.flags.set(flag.key, flag);\n }\n\n addRule(flagId: string, rule: TargetingRule): void {\n const existing = this.rules.get(flagId) || [];\n existing.push(rule);\n this.rules.set(flagId, existing);\n }\n\n addExperiment(experiment: Experiment, flagId: string): void {\n this.experiments.set(flagId, experiment);\n }\n\n async getFlag(key: string): Promise<FeatureFlag | null> {\n return this.flags.get(key) || null;\n }\n\n async getRules(flagId: string): Promise<TargetingRule[]> {\n return this.rules.get(flagId) || [];\n }\n\n async getActiveExperiment(flagId: string): Promise<Experiment | null> {\n return this.experiments.get(flagId) || null;\n }\n\n async getExperimentAssignment(\n experimentId: string,\n subjectType: string,\n subjectId: string\n ): Promise<string | null> {\n const key = `${experimentId}:${subjectType}:${subjectId}`;\n return this.assignments.get(key) || null;\n }\n\n async saveExperimentAssignment(\n experimentId: string,\n subjectType: string,\n subjectId: string,\n variant: string\n ): Promise<void> {\n const key = `${experimentId}:${subjectType}:${subjectId}`;\n this.assignments.set(key, variant);\n }\n\n clear(): void {\n this.flags.clear();\n this.rules.clear();\n this.experiments.clear();\n this.assignments.clear();\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lssm/lib.feature-flags",
3
- "version": "0.0.0-canary-20251219202229",
3
+ "version": "0.0.0-canary-20251220015515",
4
4
  "description": "Feature flags and experiments module for ContractSpec applications",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -18,13 +18,13 @@
18
18
  "lint:check": "eslint src"
19
19
  },
20
20
  "dependencies": {
21
- "@lssm/lib.schema": "0.0.0-canary-20251219202229",
22
- "@lssm/lib.contracts": "0.0.0-canary-20251219202229",
21
+ "@lssm/lib.schema": "0.0.0-canary-20251220015515",
22
+ "@lssm/lib.contracts": "0.0.0-canary-20251220015515",
23
23
  "zod": "^4.1.13"
24
24
  },
25
25
  "devDependencies": {
26
- "@lssm/tool.typescript": "0.0.0-canary-20251219202229",
27
- "@lssm/tool.tsdown": "0.0.0-canary-20251219202229",
26
+ "@lssm/tool.typescript": "0.0.0-canary-20251220015515",
27
+ "@lssm/tool.tsdown": "0.0.0-canary-20251220015515",
28
28
  "typescript": "^5.9.3"
29
29
  },
30
30
  "exports": {
@@ -55,7 +55,13 @@
55
55
  "./events": "./dist/events.js",
56
56
  "./feature-flags.feature": "./dist/feature-flags.feature.js",
57
57
  "./*": "./*"
58
- }
58
+ },
59
+ "registry": "https://registry.npmjs.org/"
59
60
  },
60
- "license": "MIT"
61
+ "license": "MIT",
62
+ "repository": {
63
+ "type": "git",
64
+ "url": "https://github.com/lssm-tech/contractspec.git",
65
+ "directory": "packages/libs/feature-flags"
66
+ }
61
67
  }