{"version":3,"sources":["../src/prompt-experiments.ts"],"names":["hashString","input","h","stickyResolver","variants","context","weights","v","total","a","b","roll","acc","i","createPromptExperiment","config","fallback","variantId","fellBack","decision","variant","fb","flagResolver","getVariant","experimentName","id","match"],"mappings":"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","file":"prompt-experiments.cjs","sourcesContent":["export interface PromptVariant<TResult = string> {\n  /** Variant id — matches a feature-flag payload / flag value. */\n  id: string\n  /** The prompt value to serve when this variant is picked. */\n  prompt: TResult\n  /** Relative weight used by the default sticky-hash resolver. Default 1. */\n  weight?: number\n}\n\nexport interface PromptExperimentContext {\n  /**\n   * Opaque stable identifier used for sticky assignment — user id,\n   * session id, or any business key. Without this, resolution is\n   * random-per-call (fine for smoke tests, bad for A/B cohorts).\n   */\n  subjectId?: string\n  /** Pass-through metadata handed to custom resolvers. */\n  metadata?: Record<string, unknown>\n}\n\nexport type PromptResolver<TResult = string> = (\n  variants: PromptVariant<TResult>[],\n  context: PromptExperimentContext,\n) => PromptVariant<TResult> | Promise<PromptVariant<TResult>> | string | Promise<string>\n\nexport interface PromptExperiment<TResult = string> {\n  /** Experiment name — used by analytics / flag providers as the key. */\n  name: string\n  variants: PromptVariant<TResult>[]\n  /**\n   * Pick a variant. Plug in your feature-flag client\n   * (PostHog / GrowthBook / Unleash / custom) here. If it throws or\n   * returns an unknown id, the experiment falls back to the built-in\n   * sticky-hash resolver.\n   */\n  resolve: PromptResolver<TResult>\n  /** Fires on every resolution — feed your analytics pipeline. */\n  onExposure?: (decision: {\n    name: string\n    variantId: string\n    subjectId?: string\n    fallback: boolean\n  }) => void\n}\n\nexport interface PromptDecision<TResult = string> {\n  name: string\n  variantId: string\n  prompt: TResult\n  fallback: boolean\n}\n\nfunction hashString(input: string): number {\n  let h = 2166136261\n  for (let i = 0; i < input.length; i++) {\n    h ^= input.charCodeAt(i)\n    h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0\n  }\n  return h\n}\n\n/**\n * Deterministic sticky assignment: same `subjectId` always maps to\n * the same variant, weighted by `variant.weight`. When `subjectId`\n * is omitted, falls back to `Math.random()` — good enough for\n * smoke tests, not for production cohorts.\n */\nexport function stickyResolver<TResult = string>(): PromptResolver<TResult> {\n  return (variants, context) => {\n    const weights = variants.map(v => Math.max(0, v.weight ?? 1))\n    const total = weights.reduce((a, b) => a + b, 0)\n    if (total === 0) return variants[0]!\n    const roll = context.subjectId\n      ? (hashString(context.subjectId) % 10_000) / 10_000\n      : Math.random()\n    let acc = 0\n    for (let i = 0; i < variants.length; i++) {\n      acc += weights[i]! / total\n      if (roll < acc) return variants[i]!\n    }\n    return variants[variants.length - 1]!\n  }\n}\n\n/**\n * Build a prompt experiment. Call `.pick(context)` at the call site\n * to get the variant id + prompt; wire `resolve` to your flag\n * provider's variant-selection API (PostHog's `getFeatureFlagPayload`,\n * GrowthBook's `getFeatureValue`, etc.).\n */\nexport function createPromptExperiment<TResult = string>(\n  config: PromptExperiment<TResult>,\n): {\n  pick: (context?: PromptExperimentContext) => Promise<PromptDecision<TResult>>\n} {\n  if (config.variants.length === 0) {\n    throw new Error(`createPromptExperiment(\"${config.name}\"): no variants`)\n  }\n  const fallback = stickyResolver<TResult>()\n\n  return {\n    async pick(context = {}) {\n      let variantId: string | undefined\n      let fellBack = false\n      try {\n        const decision = await config.resolve(config.variants, context)\n        if (typeof decision === 'string') variantId = decision\n        else variantId = decision?.id\n      } catch {\n        fellBack = true\n      }\n\n      let variant = config.variants.find(v => v.id === variantId)\n      if (!variant) {\n        fellBack = fellBack || variantId !== undefined\n        const fb = await fallback(config.variants, context)\n        variant = typeof fb === 'string' ? config.variants.find(v => v.id === fb) : fb\n      }\n      if (!variant) variant = config.variants[0]!\n\n      config.onExposure?.({\n        name: config.name,\n        variantId: variant.id,\n        subjectId: context.subjectId,\n        fallback: fellBack,\n      })\n\n      return {\n        name: config.name,\n        variantId: variant.id,\n        prompt: variant.prompt,\n        fallback: fellBack,\n      }\n    },\n  }\n}\n\n/**\n * Convenience resolver: maps your flag client's `getVariant(name,\n * subjectId) => string | undefined` signature directly into a\n * `PromptResolver`. Works with PostHog, GrowthBook, Unleash — anything\n * that returns a string variant id.\n */\nexport function flagResolver<TResult = string>(\n  getVariant: (name: string, context: PromptExperimentContext) => string | undefined | Promise<string | undefined>,\n  experimentName: string,\n): PromptResolver<TResult> {\n  return async (variants, context) => {\n    const id = await getVariant(experimentName, context)\n    const match = id ? variants.find(v => v.id === id) : undefined\n    if (match) return match\n    // Unknown / missing variant — throw to let the picker fall back\n    // to its default sticky-hash resolver.\n    throw new Error(`flag \"${experimentName}\" returned unknown variant: ${String(id)}`)\n  }\n}\n"]}