@sentientui/react 0.22.1 → 0.22.2

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/testing/index.tsx","../src/provider.tsx","../src/weights-store.ts","../src/preview-mode.ts","../src/override-events.ts","../src/devtools-config.ts","../src/adaptive-shared.ts","../src/devtools-registry.ts","../src/testing/scenario.ts","../src/testing/handlers.ts","../src/testing/events.ts","../src/testing/resolve.ts","../src/testing/playwright.ts","../src/testing/cypress.ts"],"sourcesContent":["import type { ReactElement } from 'react';\nimport { render, type RenderOptions, type RenderResult } from '@testing-library/react';\nimport { AdaptiveProvider } from '../provider.js';\nimport { applyScenario, resetScenario, type SentientScenario } from './scenario.js';\n\nexport type { SentientScenario };\nexport { applyScenario, resetScenario };\nexport { scenarioToHandlers } from './handlers.js';\nexport { resolveScenario, type ResolvedResponse } from './resolve.js';\nexport { getSentientEvents, clearSentientEvents, hasFiredGoal, type CapturedEvent } from './events.js';\nexport type { ScenarioWeight, ScenarioApiOverride } from './scenario.js';\nexport { mockSentient } from './playwright.js';\nexport { mockSentientCypress } from './cypress.js';\n\n/**\n * Render `ui` under a SentientUI provider configured for tests: consent is off,\n * so the SDK never initialises a client and every <Adaptive> renders its control\n * variant with zero network. A scenario forces specific variants/layout.\n */\nexport function renderWithSentient(\n ui: ReactElement,\n scenario: SentientScenario = {},\n options?: RenderOptions,\n): RenderResult {\n applyScenario(scenario);\n return render(\n <AdaptiveProvider apiKey=\"pk_test\" context=\"saas\" consent={false}>\n {ui}\n </AdaptiveProvider>,\n options,\n );\n}\n\n/** Call in a test setup file to reset forced state after each test. */\nexport function setupSentientTests(): void {\n const g = globalThis as unknown as { afterEach?: (fn: () => void) => void };\n g.afterEach?.(() => resetScenario());\n}\n","'use client';\n\ndeclare const process: { env?: { NODE_ENV?: string } } | undefined;\n\nimport {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n useSyncExternalStore,\n type ReactNode,\n} from 'react';\nimport {\n detectDeviceClass,\n detectTrafficSource,\n init,\n type SentientClient,\n type SentientConfig,\n type SlotResult,\n} from '@sentientui/core';\nimport { update as updateWeightsStore, type ComponentWeights } from './weights-store.js';\nimport { getPreviewMode, subscribePreview, createPreviewClient } from './preview-mode.js';\nimport { subscribeOverridesChanged } from './override-events.js';\nimport { publishDevtoolsConfig } from './devtools-config.js';\nimport { registerSections } from './devtools-registry.js';\n\n/**\n * Mirrors the segment derivation inside core `init()` so the cache key used\n * by `useAssignment` always matches the key `assign()` writes under. Before\n * this, the context defaulted to 'desktop:direct' while core used the\n * detected segment — a systematic cache miss for every integration that\n * didn't pass `sessionSegment` explicitly.\n */\nfunction deriveDefaultSegment(): string {\n if (typeof window === 'undefined') return 'desktop:direct';\n try {\n const device = detectDeviceClass(navigator.userAgent ?? '');\n const source = detectTrafficSource(document.referrer ?? '', window.location.origin);\n return `${device}:${source}`;\n } catch {\n return 'desktop:direct';\n }\n}\n\nconst DEFAULT_API_BASE_URL = 'https://api.sentient-ui.com/v1';\n\n/** How to render adaptive slots during SSR when assignments are not preloaded. */\nexport type SsrFallback = 'first' | 'none';\n\ntype AdaptiveContextValue = {\n client: SentientClient | null;\n // The publishable API key (pk_…). Historically named `projectId` because the\n // API uses it as the project identifier on the wire, but it is the API key,\n // not the project UUID. Field renamed for clarity.\n apiKey: string;\n initialAssignments: Record<string, string>;\n sessionSegment: string;\n ssrFallback: SsrFallback;\n onAssignment: ((componentId: string, variantId: string) => void) | undefined;\n initialLayoutOrder: string[] | null;\n initialSlots: Record<string, SlotResult>;\n initialPersona: { persona: string; confidence: number } | null;\n apiBaseUrl: string;\n debug: boolean;\n};\n\nconst AdaptiveContext = createContext<AdaptiveContextValue>({\n client: null,\n apiKey: '',\n initialAssignments: {},\n sessionSegment: 'desktop:direct',\n ssrFallback: 'first',\n onAssignment: undefined,\n initialLayoutOrder: null,\n initialSlots: {},\n initialPersona: null,\n apiBaseUrl: DEFAULT_API_BASE_URL,\n debug: false,\n});\n\nexport type AdaptiveProviderProps = {\n apiKey: string;\n context: SentientConfig['context'];\n debug?: boolean;\n /**\n * SSR-preloaded assignments from `preloadAssignments()` / `loadAdaptiveAssignments()`.\n * Passed through to `useAssignment` as synchronous initial state so crawlers and\n * the first paint see real content (recommended for SEO).\n */\n initialAssignments?: Record<string, string>;\n /**\n * Bandit segment from SSR (`device:source`). Keeps cache, assign, and worker\n * weights on one row — must match `loadAdaptiveAssignments` / session upsert.\n */\n sessionSegment?: string;\n /**\n * When no `initialAssignments` exist for a component, `'first'` renders\n * `variantIds[0]` in server HTML (safe default for SEO). Use `'none'` only for\n * decorative slots marked `clientOnly`.\n * @default 'first'\n */\n ssrFallback?: SsrFallback;\n /**\n * Consent gate. When `false` the SDK is not initialised and no events are\n * sent. Flip to `true` (e.g. after the user accepts the cookie banner) to\n * initialise and begin tracking.\n */\n consent?: boolean;\n /**\n * Where to read the visitor's consent decision from, so the SDK can gate and\n * un-gate itself instead of the host app wiring `grantConsent()` by hand.\n *\n * Keep your own banner or CMP — this only tells us how to observe it. The\n * provider reads the source on mount, re-reads it whenever `event` fires, and\n * initialises the moment it grants. Nothing is requested and no cookie is set\n * until then, and there is no page reload.\n *\n * Because the provider owns the whole lifecycle there is no ordering rule to\n * get right: pass this instead of managing `consent` yourself.\n *\n * @example // cookie written by your own banner\n * consentFrom={{ cookie: 'cookie_consent', value: 'accepted', event: 'consent-decided' }}\n * @example // a CMP that exposes an object rather than a cookie\n * consentFrom={{ check: () => window.Cookiebot?.consent?.statistics === true,\n * event: 'CookiebotOnAccept' }}\n */\n consentFrom?: {\n /** Cookie to read. Granted when its value equals `value`. */\n cookie?: string;\n /** Cookie value that means granted. @default 'accepted' */\n value?: string;\n /** Predicate for CMPs with a JS API. Takes precedence over `cookie`. */\n check?: () => boolean;\n /**\n * `window` event that signals a decision. The source is re-read when it\n * fires — the event's payload is never trusted — so any CMP's event works.\n */\n event?: string;\n };\n /**\n * Behavior before consent is granted. Pass `'statistical_winner'` to serve the\n * best-performing variant via `GET /v1/winner` with zero tracking while the\n * consent banner is showing. Requires `consent: false`.\n * @see SentientConfig.preConsentBehavior\n */\n preConsentBehavior?: 'statistical_winner' | 'control';\n /**\n * Honor the browser's Do Not Track signal. Defaults to `true`: when DNT is\n * enabled the SDK sets no cookies and sends no tracking data (overriding\n * `consent: true`). Set `false` to make your own consent gate authoritative.\n * @see SentientConfig.respectDoNotTrack\n */\n respectDoNotTrack?: boolean;\n /**\n * Called once per component the first time a variant is resolved for that\n * component in this session. Use to forward assignments to your own analytics\n * (Mixpanel, PostHog, Segment, etc.) without having to wrap `useAssignment`.\n */\n onAssignment?: (componentId: string, variantId: string) => void;\n /**\n * SSR-preloaded section order from `loadAdaptiveDecision()`.\n * Pass the `layoutOrder` field from `DecideResult`. When set,\n * `useLayoutOrder()` returns this on first render so there is no layout shift.\n */\n initialLayoutOrder?: string[] | null;\n /**\n * The section ids the app declares as reorderable, independent of any\n * decision. `AdaptiveRoot` forwards its `sections` prop here.\n *\n * Devtools reads this to offer layout previewing: a page whose decision was\n * gated by consent, or timed out, has no `initialLayoutOrder`, and registering\n * only that left the layout panel empty in exactly the situation you reach for\n * it — running the site locally before accepting a cookie banner.\n */\n declaredSections?: string[];\n /**\n * SSR-preloaded slot results from `loadAdaptiveDecision()` (the `slots`\n * field of its result). Guarantees `useAdaptiveTokens`/`AdaptiveGroup`\n * render the decided arm in server HTML — zero flicker, hydration-safe.\n */\n initialSlots?: Record<string, SlotResult>;\n /**\n * Persona decided during SSR (`persona` + `confidence` fields of\n * `loadAdaptiveDecision()`'s result). Adopted by the core client;\n * rendered into html attributes only by `SentientPersonaScript`.\n */\n initialPersona?: { persona: string; confidence: number };\n /**\n * Base URL of the Sentient API (no trailing slash). Read by the devtools\n * panel for /v1/explain and by future client helpers. Defaults to the\n * hosted API.\n */\n apiBaseUrl?: string;\n /**\n * Session ID generated during SSR (the `sessionId` field returned by\n * `loadAdaptiveAssignments` / `loadAdaptiveDecision`). When provided and no\n * existing session cookie or localStorage entry is found, the client adopts\n * this ID so events and goals are attributed to the same session the server\n * used for variant assignment.\n */\n ssrSessionId?: string;\n /**\n * ISO 3166-1 alpha-2 country code. Pass the value of the `CF-IPCountry`\n * header from your Next.js server component to populate country on landing\n * sessions without client-side geo lookup.\n */\n country?: string;\n /**\n * Keyless local mode. 'auto' (default) simulates decisions on-device in\n * development builds when no valid API key is configured; `true` forces the\n * local engine; `false` restores the silent keyless no-op.\n * @see SentientConfig.localMode\n */\n localMode?: 'auto' | boolean;\n /**\n * DOM graph scanning + page-structure sync. ON by default: the provider\n * dynamically loads `@sentientui/core/graph` and uses its graph-capable\n * `init()` for the single client, so the SDK scans your page structure,\n * auto-detects semantic sections, and syncs them to power personas and the\n * dashboard graph page. Pass `false` to keep the lean bundle only.\n */\n enableGraph?: boolean;\n /**\n * Include captured heading / DOM text in graph sync payloads. OFF by default.\n * Only applies when graph scanning is enabled (`enableGraph` not `false`).\n */\n captureDomText?: boolean;\n /**\n * Behavioral engagement capture (per-section dwell/scroll + semantic section\n * registration) powering personas. ON by default — pass `false` to disable.\n * Never runs for a DNT/GPC or consent-gated visitor (no client → no capture).\n */\n engagement?: boolean;\n children: ReactNode;\n};\n\n/**\n * Watches a {@link AdaptiveProviderProps.consentFrom} source and reports whether\n * it currently grants consent. Returns false (and subscribes to nothing) when no\n * source is configured.\n */\nfunction useConsentSource(source: AdaptiveProviderProps['consentFrom']): boolean {\n const [granted, setGranted] = useState(false);\n\n // The source is usually an inline object literal, so its identity changes\n // every render. Read it through a ref and key the effect on its primitives,\n // otherwise the listener would be torn down and re-added on every render.\n const sourceRef = useRef(source);\n sourceRef.current = source;\n\n const { cookie, value, event } = source ?? {};\n const hasCheck = typeof source?.check === 'function';\n\n useEffect(() => {\n const read = (): boolean => {\n const s = sourceRef.current;\n if (!s) return false;\n if (s.check) return s.check() === true;\n if (!s.cookie || typeof document === 'undefined') return false;\n const want = `${s.cookie}=${s.value ?? 'accepted'}`;\n return document.cookie.split('; ').some((c) => c.trim() === want);\n };\n\n if (read()) {\n setGranted(true);\n return;\n }\n if (!event) return;\n\n // Re-read rather than trusting the event payload, so this works with any\n // CMP's event shape (CookiebotOnAccept, OneTrustGroupsUpdated, …) and a\n // \"declined\" decision correctly leaves us gated.\n const onDecision = (): void => {\n if (read()) setGranted(true);\n };\n window.addEventListener(event, onDecision);\n return () => window.removeEventListener(event, onDecision);\n }, [cookie, value, event, hasCheck]);\n\n return granted;\n}\n\n/**\n * Initialises the Sentient core SDK in a useEffect (SSR-safe) and exposes the\n * client via React context. Re-initialises when consent changes.\n */\nexport function AdaptiveProvider(props: AdaptiveProviderProps): JSX.Element {\n const [client, setClient] = useState<SentientClient | null>(null);\n // Derived once per mount: identical to what core init() computes, so cache\n // reads (context segment) and cache writes (core segment) always agree.\n const [sessionSegment] = useState(() => props.sessionSegment ?? deriveDefaultSegment());\n // Devtools preview: when on, expose an event-suppressing client so previewing\n // variants/personas writes nothing. Off by default (inert in production).\n const [previewOn, setPreviewOn] = useState(getPreviewMode());\n useEffect(() => subscribePreview(() => setPreviewOn(getPreviewMode())), []);\n\n // When a consentFrom source is configured it owns the gate: start closed and\n // open only once the source grants. An explicit consent={true} (e.g. resolved\n // from the cookie on the server by AdaptiveRoot) short-circuits it, so a\n // returning visitor isn't gated waiting for a client-side re-read.\n const sourceGranted = useConsentSource(props.consentFrom);\n const consent = props.consentFrom ? props.consent === true || sourceGranted : props.consent;\n\n useEffect(() => {\n // When consent is explicitly false with no preConsentBehavior, tear down any existing client.\n if (consent === false && !props.preConsentBehavior) {\n setClient((prev: SentientClient | null) => {\n prev?.destroy();\n return null;\n });\n return;\n }\n\n const config = {\n apiKey: props.apiKey,\n context: props.context,\n debug: props.debug,\n initialAssignments: props.initialAssignments,\n sessionSegment,\n consent,\n preConsentBehavior: props.preConsentBehavior,\n respectDoNotTrack: props.respectDoNotTrack,\n ssrSessionId: props.ssrSessionId,\n country: props.country,\n localMode: props.localMode,\n initialSlots: props.initialSlots,\n initialPersona: props.initialPersona,\n ingestUrl: props.apiBaseUrl ? `${props.apiBaseUrl.replace(/\\/$/, '')}/events` : undefined,\n };\n\n // Track the client created by this effect run so cleanup destroys exactly\n // the right one, and so a late-resolving dynamic import can bail if the\n // effect was already torn down (unmount / consent change).\n let cancelled = false;\n let created: SentientClient | null = null;\n let stopEngagement: (() => void) | null = null;\n\n // Engagement capture (default ON): per-section dwell/scroll + semantic\n // section registration, lazy-loaded so the lean bundle stays lean. Only\n // ever started for an initialised client, so consent/DNT gates are\n // inherited (and the capture module re-checks DNT internally).\n const startEngagement = (c: SentientClient): void => {\n if (props.engagement === false) return;\n void import('@sentientui/core/engagement').then(({ startEngagementCapture }) => {\n if (cancelled) return;\n stopEngagement = startEngagementCapture(c, {\n apiKey: props.apiKey,\n apiBase: props.apiBaseUrl ? props.apiBaseUrl.replace(/\\/$/, '') : undefined,\n });\n });\n };\n\n if (props.enableGraph !== false) {\n // Graph scanning is the default — load the graph entry dynamically so the\n // scanner never lands in the lean bundle. The provider still creates ONE\n // client (graph-capable). Pass enableGraph={false} for the lean client.\n void import('@sentientui/core/graph').then(({ init: initGraph }) => {\n if (cancelled) return;\n created = initGraph({ ...config, graph: true, captureDomText: props.captureDomText === true });\n setClient(created);\n startEngagement(created);\n });\n } else {\n created = init(config);\n setClient(created);\n startEngagement(created);\n }\n\n return () => {\n cancelled = true;\n stopEngagement?.();\n // dispose, not destroy: effect cleanup runs on unmount, StrictMode's\n // dev double-invoke, and consent re-init — the visitor identity must\n // survive all of those. Full destroy() happens only on the explicit\n // consent-revocation branch above.\n created?.dispose();\n };\n // Re-init when consent changes — whether that came from the prop or from a\n // consentFrom source granting. Other props (incl. enableGraph) are\n // intentionally stable for a session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [consent]);\n\n // Poll /v1/weights every 60 s so long-lived sessions see updated bandit weights\n // without a page reload. useAssignment subscribers react via weights-store.\n useEffect(() => {\n if (!client) return;\n let cancelled = false;\n const poll = async (): Promise<void> => {\n if (cancelled) return;\n let entries;\n try {\n entries = await client.fetchWeights();\n } catch {\n // Network/transient error: skip this cycle and retry on the next\n // interval. Swallowed deliberately so a failed poll never surfaces as\n // an unhandled rejection.\n return;\n }\n if (cancelled) return;\n for (const entry of entries) {\n const weights: ComponentWeights = {\n componentId: entry.componentId,\n updatedAt: entry.updatedAt,\n variants: entry.variants.map((v) => ({\n variantId: v.variantId,\n pulls: v.pulls,\n avgReward: v.avgReward ?? 0,\n })),\n };\n updateWeightsStore(entry.componentId, weights);\n }\n };\n void poll();\n const timerId = setInterval(() => void poll(), 60_000);\n return () => {\n cancelled = true;\n clearInterval(timerId);\n };\n }, [client]);\n\n const ssrFallback = props.ssrFallback ?? 'first';\n // Strip a trailing slash so consumers (devtools /explain, useAdaptiveApiBaseUrl)\n // build URLs the same way the core client does (it strips before appending\n // /events and /section-map) — otherwise an apiBaseUrl ending in \"/\" yields a\n // double slash like \".../v1//explain\".\n const apiBaseUrl = (props.apiBaseUrl ?? DEFAULT_API_BASE_URL).replace(/\\/$/, '');\n\n // The init effect above re-runs only on `props.consent`: apiKey / context /\n // country / apiBaseUrl are captured once and are deliberately stable for the\n // session, so changing them at runtime silently no-ops. That silence is\n // surprising — warn (dev only) when one actually changes value after init, so\n // the no-op is visible. To apply a new value, remount the provider (e.g. a\n // changing React `key`).\n const frozenConfigRef = useRef<{\n apiKey: string;\n context: SentientConfig['context'];\n country: string | undefined;\n apiBaseUrl: string;\n } | null>(null);\n useEffect(() => {\n if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'production') return;\n const current = { apiKey: props.apiKey, context: props.context, country: props.country, apiBaseUrl };\n const prev = frozenConfigRef.current;\n frozenConfigRef.current = current;\n if (prev === null) return; // first run: capture the frozen baseline, nothing to compare\n for (const key of ['apiKey', 'context', 'country', 'apiBaseUrl'] as const) {\n if (!Object.is(prev[key], current[key])) {\n console.warn(\n `[sentient] AdaptiveProvider: \\`${key}\\` changed after initialisation, but the SDK client is stable for the session and only re-inits on \\`consent\\` — the new value is ignored. Remount the provider (e.g. via a changing \\`key\\` prop) to apply it.`,\n );\n }\n }\n }, [props.apiKey, props.context, props.country, apiBaseUrl]);\n\n // Publish devtools config through window: the /devtools entry is a separate\n // bundle and cannot read this provider's context instance. Dev-only.\n useEffect(() => {\n if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'production') return;\n publishDevtoolsConfig({\n apiKey: props.apiKey,\n apiBaseUrl,\n isLocal: client?.isLocal === true,\n });\n }, [client, props.apiKey, apiBaseUrl]);\n\n // Sections registry for devtools /v1/explain + local simulation. The decided\n // order wins when there is one (it is what the page is actually rendering);\n // the declared list is the fallback so the layout panel still knows what is\n // reorderable when no decision arrived.\n useEffect(() => {\n const decided = props.initialLayoutOrder;\n if (decided && decided.length > 0) {\n registerSections(decided);\n return;\n }\n if (props.declaredSections && props.declaredSections.length > 0) {\n registerSections(props.declaredSections);\n }\n }, [props.initialLayoutOrder, props.declaredSections]);\n\n // The client exposed to consumers — wrapped to suppress events while previewing.\n const exposedClient = useMemo(\n () => (client && previewOn ? createPreviewClient(client) : client),\n [client, previewOn],\n );\n\n // Memoized so unrelated parent re-renders don't cascade through every\n // useSentient / useAssignment consumer via a fresh context object.\n const value = useMemo<AdaptiveContextValue>(\n () => ({\n client: exposedClient,\n apiKey: props.apiKey,\n initialAssignments: props.initialAssignments ?? {},\n sessionSegment,\n ssrFallback,\n onAssignment: props.onAssignment,\n initialLayoutOrder: props.initialLayoutOrder ?? null,\n initialSlots: props.initialSlots ?? {},\n initialPersona: props.initialPersona ?? null,\n apiBaseUrl,\n debug: props.debug ?? false,\n }),\n [\n exposedClient,\n props.apiKey,\n props.initialAssignments,\n sessionSegment,\n ssrFallback,\n props.onAssignment,\n props.initialLayoutOrder,\n props.initialSlots,\n props.initialPersona,\n apiBaseUrl,\n props.debug,\n ],\n );\n\n return (\n <AdaptiveContext.Provider value={value}>\n {props.children}\n </AdaptiveContext.Provider>\n );\n}\n\n/**\n * Returns the SentientClient, or null until the provider has finished\n * initialising on the client.\n */\nexport function useSentient(): SentientClient | null {\n return useContext(AdaptiveContext).client;\n}\n\n/** Internal: publishable API key carried alongside the client. */\nexport function useAdaptiveApiKey(): string {\n return useContext(AdaptiveContext).apiKey;\n}\n\n/** Internal: SSR-preloaded assignments for hydration-safe first render. */\nexport function useInitialAssignments(): Record<string, string> {\n return useContext(AdaptiveContext).initialAssignments;\n}\n\n/** Internal: bandit segment aligned with SSR session upsert. */\nexport function useSessionSegment(): string {\n return useContext(AdaptiveContext).sessionSegment;\n}\n\n/** Internal: SSR fallback strategy when a slot has no preloaded assignment. */\nexport function useSsrFallback(): SsrFallback {\n return useContext(AdaptiveContext).ssrFallback;\n}\n\n/** Internal: forwarding hook for consumer analytics integration. */\nexport function useOnAssignment(): ((componentId: string, variantId: string) => void) | undefined {\n return useContext(AdaptiveContext).onAssignment;\n}\n\n/** Internal: debug flag from the provider config, for dev-only diagnostic logging. */\nexport function useDebug(): boolean {\n return useContext(AdaptiveContext).debug;\n}\n\n/**\n * Returns the persona-specific section order from SSR, or null when no\n * sections were declared on AdaptiveRoot or reliability is below threshold.\n * Devtools/testing can force it via `window.__sentient_layout_override`;\n * consumers re-render when the devtools notifies an override change.\n */\nexport function useLayoutOrder(): string[] | null {\n const contextOrder = useContext(AdaptiveContext).initialLayoutOrder;\n const override = useSyncExternalStore(\n subscribeOverridesChanged,\n () =>\n typeof window === 'undefined'\n ? null\n : ((window as unknown as { __sentient_layout_override?: string[] })\n .__sentient_layout_override ?? null),\n () => null,\n );\n return override ?? contextOrder;\n}\n\n/** Internal: SSR-preloaded slot results for hydration-safe first render. */\nexport function useInitialSlots(): Record<string, SlotResult> {\n return useContext(AdaptiveContext).initialSlots;\n}\n\n/** Internal: SSR-decided persona carried alongside the client. */\nexport function useInitialPersona(): { persona: string; confidence: number } | null {\n return useContext(AdaptiveContext).initialPersona;\n}\n\n/** Internal: configured API base URL (devtools fetches /v1/explain against this, never a relative URL). */\nexport function useAdaptiveApiBaseUrl(): string {\n return useContext(AdaptiveContext).apiBaseUrl;\n}\n","/** Per-component weights store with isolated subscriptions. */\n\nexport type VariantWeight = {\n variantId: string;\n pulls: number;\n avgReward: number;\n};\n\nexport type ComponentWeights = {\n componentId: string;\n variants: VariantWeight[];\n updatedAt: number;\n};\n\ntype Listener = (weights: ComponentWeights) => void;\n\nconst store = new Map<string, ComponentWeights>();\nconst listeners = new Map<string, Set<Listener>>();\n\n/**\n * Subscribes a listener to a single component. Returns an unsubscribe function.\n * Updates to other components never trigger this listener.\n */\nexport function subscribe(componentId: string, cb: Listener): () => void {\n let set = listeners.get(componentId);\n if (!set) {\n set = new Set();\n listeners.set(componentId, set);\n }\n set.add(cb);\n return () => {\n set!.delete(cb);\n if (set!.size === 0) listeners.delete(componentId);\n };\n}\n\n/**\n * Replaces the weights for a component and notifies only that component's\n * subscribers.\n */\nexport function update(componentId: string, weights: ComponentWeights): void {\n store.set(componentId, weights);\n const set = listeners.get(componentId);\n if (!set) return;\n for (const cb of set) {\n try {\n cb(weights);\n } catch {\n /* never throw to other listeners */\n }\n }\n}\n\n/**\n * Returns the latest known weights for a component, or null if none seen.\n */\nexport function getWeights(componentId: string): ComponentWeights | null {\n return store.get(componentId) ?? null;\n}\n\n/** Test-only: wipe the entire store. */\nexport function _resetWeightsStore(): void {\n store.clear();\n listeners.clear();\n}\n","import type { SentientClient } from '@sentientui/core';\n\ntype PreviewState = { on: boolean; listeners: Set<() => void> };\nconst ssrFallback: PreviewState = { on: false, listeners: new Set() };\n\n// Window-backed: the /devtools entry (a separate bundle) toggles preview mode\n// and the provider (main bundle) must observe it.\nfunction state(): PreviewState {\n if (typeof window === 'undefined') return ssrFallback;\n const w = window as unknown as { __sentient_preview?: PreviewState };\n if (!w.__sentient_preview) w.__sentient_preview = { on: false, listeners: new Set() };\n return w.__sentient_preview;\n}\n\nexport function setPreviewMode(on: boolean): void {\n const s = state();\n if (s.on === on) return;\n s.on = on;\n for (const fn of s.listeners) fn();\n}\n\nexport function getPreviewMode(): boolean {\n return state().on;\n}\n\nexport function subscribePreview(fn: () => void): () => void {\n const listeners = state().listeners;\n listeners.add(fn);\n return () => {\n listeners.delete(fn);\n };\n}\n\n/**\n * Wraps a client so it writes nothing: reads pass through, all emitters no-op.\n * Used while previewing variants/personas so no `variant_assigned`, goal, or\n * session events are sent.\n */\nexport function createPreviewClient(inner: SentientClient): SentientClient {\n return {\n isLocal: inner.isLocal,\n track: () => undefined,\n goal: () => undefined,\n componentGoal: () => undefined,\n identify: () => undefined,\n fetchWeights: () => Promise.resolve([]),\n getAssignment: (componentId, segment) => inner.getAssignment(componentId, segment),\n assign: (componentId, variantIds, agentData, agentDataByVariant) =>\n inner.assign(componentId, variantIds, agentData, agentDataByVariant),\n // Reads pass through; decide is a write (slot decisions persist server-side)\n // so preview mode never issues it.\n decide: () => Promise.resolve(null),\n getSlotResult: (slotId) => inner.getSlotResult(slotId),\n getPersona: () => inner.getPersona(),\n getGraph: () => inner.getGraph(),\n dispose: () => inner.dispose(),\n destroy: () => inner.destroy(),\n };\n}\n","/**\n * Cross-bundle re-render bus for devtools overrides. The main entry and the\n * /devtools entry are separate bundles, so state and notifications go through\n * window (a version counter + a DOM event) — never module-local state.\n */\nconst EVENT = 'sentient:overrides-changed';\n\ntype VersionWindow = Window & { __sentient_overrides_version?: number };\n\nexport function getOverridesVersion(): number {\n if (typeof window === 'undefined') return 0;\n return (window as VersionWindow).__sentient_overrides_version ?? 0;\n}\n\nexport function notifyOverridesChanged(): void {\n if (typeof window === 'undefined') return;\n const w = window as VersionWindow;\n w.__sentient_overrides_version = (w.__sentient_overrides_version ?? 0) + 1;\n window.dispatchEvent(new Event(EVENT));\n}\n\nexport function subscribeOverridesChanged(fn: () => void): () => void {\n if (typeof window === 'undefined') return () => undefined;\n window.addEventListener(EVENT, fn);\n return () => window.removeEventListener(EVENT, fn);\n}\n","/** Provider → devtools config handoff. Window-backed: the /devtools entry is a\n * separate bundle and cannot share the provider's React context instance. */\nexport type DevtoolsConfig = {\n apiKey: string;\n apiBaseUrl: string;\n isLocal: boolean;\n};\n\ntype ConfigWindow = Window & { __sentient_devtools_config?: DevtoolsConfig };\n\nexport function publishDevtoolsConfig(config: DevtoolsConfig): void {\n if (typeof window === 'undefined') return;\n (window as ConfigWindow).__sentient_devtools_config = config;\n}\n\nexport function readDevtoolsConfig(): DevtoolsConfig | null {\n if (typeof window === 'undefined') return null;\n return (window as ConfigWindow).__sentient_devtools_config ?? null;\n}\n","import type { SentientClient } from '@sentientui/core';\n\ndeclare const process: { env?: { NODE_ENV?: string } } | undefined;\n\n/**\n * True unless NODE_ENV is 'production'. Never `process?.env` — optional\n * chaining still throws ReferenceError on an undeclared global, and browsers\n * without a bundler shim (raw esbuild, vanilla script tags) have no `process`.\n */\nexport function isDevBuild(): boolean {\n return typeof process === 'undefined' || process.env?.NODE_ENV !== 'production';\n}\n\nexport type ScrollDepthGoal = { type: 'scroll_depth'; threshold: number; value?: number };\nexport type ClickGoal = { type: 'click'; selector?: string; value?: number };\nexport type FormSubmitGoal = { type: 'form_submit'; value?: number };\nexport type CompositeGoal = { type: 'composite'; all: GoalConfig[] };\nexport type WeightedStep = { goal: GoalConfig; name: string; weight: number };\nexport type WeightedCompositeGoal = { type: 'weighted_composite'; steps: WeightedStep[] };\nexport type GoalConfig = ScrollDepthGoal | ClickGoal | FormSubmitGoal | CompositeGoal | WeightedCompositeGoal;\n\nexport function normalizeGoal(goal: string | GoalConfig): GoalConfig {\n if (typeof goal === 'string') return { type: 'click' };\n return goal;\n}\n\n/** The goalType label events are recorded under (named goal or config type). */\nexport function goalLabelOf(goal: string | GoalConfig): string {\n return typeof goal === 'string' ? goal : goal.type;\n}\n\n/** Static revenue value declared on a simple goal config, if any. Composites\n * carry no value (weights and values don't mix — spec §9.4); dynamic values\n * go through the imperative hooks. */\nexport function goalValueOf(goal: string | GoalConfig): number | undefined {\n if (typeof goal === 'string') return undefined;\n if (goal.type === 'click' || goal.type === 'form_submit' || goal.type === 'scroll_depth') {\n return goal.value;\n }\n return undefined;\n}\n\nfunction isClickableTarget(el: EventTarget | null): boolean {\n if (!(el instanceof Element)) return false;\n const tag = el.tagName.toLowerCase();\n if (tag === 'a' || tag === 'button') return true;\n const role = el.getAttribute('role');\n return role === 'button';\n}\n\nfunction findClickable(start: Element, container: Element, selector?: string): boolean {\n if (selector) {\n try {\n let cursor: Element | null = start;\n while (cursor && cursor !== container) {\n if (cursor.matches(selector)) return true;\n cursor = cursor.parentElement;\n }\n } catch {\n // Invalid CSS selector — treat as no match rather than breaking all click handlers.\n }\n return false;\n }\n let cursor: Element | null = start;\n while (cursor && cursor !== container) {\n if (isClickableTarget(cursor)) return true;\n cursor = cursor.parentElement;\n }\n return false;\n}\n\nexport type GoalHandlers = {\n /** Primary goal completion. Latch-once semantics are the CALLER's job. */\n fireGoal: () => void;\n /** Weighted-composite step completion (already deduped per step here). */\n fireStep: (name: string, weight: number, stepIndex: number) => void;\n};\n\n/**\n * Attaches the goal-detection listeners `<Adaptive>`'s container uses —\n * click / form_submit / scroll_depth, composite (all-of), and\n * weighted_composite (independent steps). Returns the cleanup function.\n * Extracted from adaptive.tsx so useAdaptive / useAdaptiveTokens /\n * AdaptiveGroup wire the SAME machinery instead of duplicating it.\n */\nexport function attachGoalListeners(node: Element, goal: GoalConfig, handlers: GoalHandlers): () => void {\n // --- Weighted composite: each step fires independently as it completes ---\n if (goal.type === 'weighted_composite') {\n const firedSteps = new Set<number>();\n const wcCleanups: Array<() => void> = [];\n\n goal.steps.forEach(({ goal: sub, name: stepName, weight: stepWeight }, idx) => {\n const fireStep = (): void => {\n if (firedSteps.has(idx)) return;\n firedSteps.add(idx);\n handlers.fireStep(stepName, stepWeight, idx);\n };\n\n if (sub.type === 'click') {\n const onClick = (e: Event): void => {\n const target = e.target;\n if (!(target instanceof Element)) return;\n if (!findClickable(target, node, sub.selector)) return;\n fireStep();\n };\n node.addEventListener('click', onClick);\n wcCleanups.push(() => node.removeEventListener('click', onClick));\n return;\n }\n\n if (sub.type === 'form_submit') {\n const onSubmit = (e: Event): void => {\n if (!(e.target instanceof HTMLFormElement)) return;\n if (!node.contains(e.target)) return;\n fireStep();\n };\n node.addEventListener('submit', onSubmit);\n wcCleanups.push(() => node.removeEventListener('submit', onSubmit));\n return;\n }\n\n if (sub.type === 'scroll_depth') {\n const threshold = Math.max(0, Math.min(1, sub.threshold));\n const io = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.intersectionRatio >= threshold) {\n fireStep();\n io.disconnect();\n break;\n }\n }\n },\n { threshold: [threshold] },\n );\n io.observe(node);\n wcCleanups.push(() => io.disconnect());\n }\n });\n\n return () => {\n for (const c of wcCleanups) c();\n };\n }\n // --- End weighted composite ---\n\n const subgoals: GoalConfig[] = goal.type === 'composite' ? goal.all : [goal];\n const remaining = new Set<number>(subgoals.map((_, i) => i));\n const checkComposite = (idx: number): void => {\n remaining.delete(idx);\n if (remaining.size === 0) handlers.fireGoal();\n };\n\n const cleanups: Array<() => void> = [];\n\n subgoals.forEach((sub, idx) => {\n if (sub.type === 'click') {\n const onClick = (e: Event): void => {\n const target = e.target;\n if (!(target instanceof Element)) return;\n if (!findClickable(target, node, sub.selector)) return;\n if (goal.type === 'composite') checkComposite(idx);\n else handlers.fireGoal();\n };\n node.addEventListener('click', onClick);\n cleanups.push(() => node.removeEventListener('click', onClick));\n return;\n }\n\n if (sub.type === 'form_submit') {\n const onSubmit = (e: Event): void => {\n if (!(e.target instanceof HTMLFormElement)) return;\n if (!node.contains(e.target)) return;\n if (goal.type === 'composite') checkComposite(idx);\n else handlers.fireGoal();\n };\n node.addEventListener('submit', onSubmit);\n cleanups.push(() => node.removeEventListener('submit', onSubmit));\n return;\n }\n\n if (sub.type === 'scroll_depth') {\n const threshold = Math.max(0, Math.min(1, sub.threshold));\n const io = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.intersectionRatio >= threshold) {\n if (goal.type === 'composite') checkComposite(idx);\n else handlers.fireGoal();\n io.disconnect();\n break;\n }\n }\n },\n { threshold: [threshold] },\n );\n io.observe(node);\n cleanups.push(() => io.disconnect());\n return;\n }\n });\n\n return () => {\n for (const c of cleanups) c();\n };\n}\n\n/**\n * Funnel declaration (spec §7.4): one steps-declaration per funnel per page\n * load; membership is per component and idempotent server-side, so re-sends\n * are harmless but avoided. Only a weighted_composite carries steps — a plain\n * goal declares membership only (the funnel must exist server-side already).\n */\nconst declaredFunnels = new Set<string>();\nconst declaredMemberships = new Set<string>();\n\n/** Test-only: clears the page-load dedup sets. */\nexport function __resetFunnelDeclarations(): void {\n declaredFunnels.clear();\n declaredMemberships.clear();\n}\n\nexport function maybeDeclareFunnel(\n client: SentientClient,\n apiKey: string,\n componentId: string,\n funnelId: string,\n goal: GoalConfig,\n): void {\n const memberKey = `${funnelId}|${componentId}`;\n if (declaredMemberships.has(memberKey)) return;\n declaredMemberships.add(memberKey);\n const withSteps = goal.type === 'weighted_composite' && !declaredFunnels.has(funnelId);\n if (withSteps) declaredFunnels.add(funnelId);\n client.track({\n projectId: apiKey,\n componentId,\n // 'funnel_declared' is server-accepted but not yet in core's EventType\n // union — cast rather than touch core (its byte budget is exhausted).\n eventType: 'funnel_declared' as Parameters<SentientClient['track']>[0]['eventType'],\n payload: withSteps\n ? {\n funnelId,\n steps: (goal as WeightedCompositeGoal).steps.map((s) => ({ goalId: s.name, weight: s.weight })),\n }\n : { funnelId },\n });\n}\n\n/**\n * Records the `variant_assigned` exposure event.\n */\nexport function trackExposure(\n client: SentientClient,\n apiKey: string,\n componentId: string,\n variantId: string,\n): void {\n client.track({\n projectId: apiKey,\n componentId,\n variantId,\n eventType: 'variant_assigned',\n payload: {},\n });\n}\n","import { isDevBuild } from './adaptive-shared.js';\n\nexport type RegisteredComponent = { id: string; variantIds: string[]; goal?: string };\nexport type RegisteredSlot = {\n id: string;\n arms?: string[];\n dims?: Record<string, readonly string[]>;\n};\n\ntype RegistryState = {\n components: Map<string, RegisteredComponent>;\n slots: Map<string, RegisteredSlot>;\n sections: string[];\n listeners: Set<() => void>;\n /** Bumped on every mutation so `useSyncExternalStore` can read a stable, comparable snapshot. */\n version: number;\n};\n\n// Shared through a window global: the main entry and the /devtools entry are\n// separate bundles, each with its own copy of this module — module-local\n// state would give the devtools an always-empty registry in published apps.\nconst ssrFallback: RegistryState = {\n components: new Map(),\n slots: new Map(),\n sections: [],\n listeners: new Set(),\n version: 0,\n};\n\nfunction state(): RegistryState {\n if (typeof window === 'undefined') return ssrFallback;\n const w = window as unknown as { __sentient_registry?: RegistryState };\n if (!w.__sentient_registry) {\n w.__sentient_registry = {\n components: new Map(),\n slots: new Map(),\n sections: [],\n listeners: new Set(),\n version: 0,\n };\n }\n return w.__sentient_registry;\n}\n\nfunction emit(): void {\n // Bump BEFORE notifying so a useSyncExternalStore consumer re-reading its\n // snapshot inside the notification sees the new value and re-renders.\n state().version += 1;\n for (const fn of state().listeners) fn();\n}\n\n// The registry exists solely to feed the opt-in devtools panel, which is a\n// dev-only surface. In a production build there is no panel reading it, so\n// every register call is dead work plus a `window.__sentient_registry`\n// footprint on the customer's page. Short-circuit to a noop in production —\n// every <Adaptive>/useAdaptive/slot mount calls one of these.\nconst NOOP_UNREGISTER = (): void => undefined;\n\n/** Register (or re-register) a component. Returns an unregister function. */\nexport function registerComponent(c: RegisteredComponent): () => void {\n if (!isDevBuild()) return NOOP_UNREGISTER;\n state().components.set(c.id, c);\n emit();\n return () => {\n state().components.delete(c.id);\n emit();\n };\n}\n\n/** Register (or re-register) a slot declaration. Returns an unregister function. */\nexport function registerSlot(s: RegisteredSlot): () => void {\n if (!isDevBuild()) return NOOP_UNREGISTER;\n state().slots.set(s.id, s);\n emit();\n return () => {\n state().slots.delete(s.id);\n emit();\n };\n}\n\n/** Register the page's declared section ids (from AdaptiveRoot/provider). */\nexport function registerSections(sections: string[]): void {\n if (!isDevBuild()) return;\n state().sections = [...sections];\n emit();\n}\n\nexport function getRegistered(): RegisteredComponent[] {\n return [...state().components.values()];\n}\n\nexport function getRegisteredSlots(): RegisteredSlot[] {\n return [...state().slots.values()];\n}\n\nexport function getRegisteredSections(): string[] {\n return [...state().sections];\n}\n\nexport function subscribeRegistry(fn: () => void): () => void {\n const listeners = state().listeners;\n listeners.add(fn);\n return () => {\n listeners.delete(fn);\n };\n}\n\n/** Monotonic snapshot for `useSyncExternalStore` — changes on every registry mutation. */\nexport function getRegistryVersion(): number {\n return state().version;\n}\n","import { notifyOverridesChanged } from '../override-events.js';\n\nexport type ScenarioWeight = { variantId: string; pulls: number; avgReward: number };\nexport type ScenarioApiOverride =\n | 'error'\n | number\n | { status?: number; body?: unknown; delayMs?: number };\n\nexport type SentientScenario = {\n variants?: Record<string, string>;\n layout?: string[];\n /** Forced persona (canonical PersonaKey). Also sets the persona html attributes. */\n persona?: string;\n /** Persona confidence 0–1; buckets to low/medium/high for the html attribute. Default 1. */\n confidence?: number;\n /** Forced slot results: slot id → arm id (arms slots) or per-dim values (token slots). */\n slots?: Record<string, string | Record<string, string>>;\n weights?: Record<string, ScenarioWeight[]>;\n api?: Record<string, ScenarioApiOverride>;\n};\n\ntype ScenarioWindow = {\n __sentient_overrides?: Record<string, string>;\n __sentient_layout_override?: string[];\n __sentient_slot_overrides?: Record<string, string | Record<string, string>>;\n __sentient_persona_override?: { persona: string; confidence?: number };\n};\n\n/**\n * Confidence → band. Cutoffs pinned to @sentientui/policy `confidenceBand`\n * (<0.3 low, <0.7 medium, else high). Duplicated (not imported) because the\n * Playwright init function is serialized into the page and cannot close\n * over imports — both copies are pinned by tests.\n */\nexport function confidenceBandOf(c: number): 'low' | 'medium' | 'high' {\n return c < 0.3 ? 'low' : c < 0.7 ? 'medium' : 'high';\n}\n\n/** Apply a scenario by setting the client-forcing globals the SDK reads. */\nexport function applyScenario(scenario: SentientScenario = {}): void {\n const w = window as unknown as ScenarioWindow;\n w.__sentient_overrides = { ...(scenario.variants ?? {}) };\n if (scenario.layout) w.__sentient_layout_override = scenario.layout;\n else delete w.__sentient_layout_override;\n if (scenario.slots) w.__sentient_slot_overrides = { ...scenario.slots };\n else delete w.__sentient_slot_overrides;\n if (scenario.persona) {\n w.__sentient_persona_override = {\n persona: scenario.persona,\n confidence: scenario.confidence ?? 1,\n };\n try {\n const d = document.documentElement;\n d.setAttribute('data-sentient-persona', scenario.persona);\n d.setAttribute('data-sentient-confidence', confidenceBandOf(scenario.confidence ?? 1));\n } catch {\n /* no DOM (node env) — the override globals still apply */\n }\n } else {\n delete w.__sentient_persona_override;\n }\n // Ring the override bus so hooks already mounted (useAssignment /\n // useSlotResult / useAdaptivePersona subscribe via useSyncExternalStore)\n // re-render immediately — otherwise forcing state mid-test only takes effect\n // on the next unrelated render.\n notifyOverridesChanged();\n}\n\n/** Clear all forced state. */\nexport function resetScenario(): void {\n const w = window as unknown as ScenarioWindow;\n delete w.__sentient_overrides;\n delete w.__sentient_layout_override;\n delete w.__sentient_slot_overrides;\n delete w.__sentient_persona_override;\n try {\n document.documentElement.removeAttribute('data-sentient-persona');\n document.documentElement.removeAttribute('data-sentient-confidence');\n } catch {\n /* no DOM */\n }\n notifyOverridesChanged();\n}\n","import { http, HttpResponse } from 'msw';\nimport type { RequestHandler } from 'msw';\nimport { resolveScenario } from './resolve.js';\nimport type { SentientScenario } from './scenario.js';\n\n/** Turn a scenario into MSW handlers stubbing every SDK endpoint + capturing events. */\nexport function scenarioToHandlers(scenario: SentientScenario = {}): RequestHandler[] {\n return [\n http.all('*/v1/*', async ({ request }) => {\n const bodyText =\n request.method === 'GET' || request.method === 'HEAD' ? null : await request.text();\n const r = await resolveScenario(scenario, request.method, request.url, bodyText);\n if (!r) return undefined; // not a stubbed route — let MSW handle passthrough\n if (r.json === undefined) return new HttpResponse(null, { status: r.status });\n return HttpResponse.json(r.json as object, { status: r.status });\n }),\n ];\n}\n","export type CapturedEvent = {\n eventType: string;\n goalType?: string;\n componentId?: string;\n variantId?: string;\n [k: string]: unknown;\n};\n\nconst captured: CapturedEvent[] = [];\n\nexport function recordEvent(e: CapturedEvent): void {\n captured.push(e);\n}\n\nexport function getSentientEvents(): CapturedEvent[] {\n return [...captured];\n}\n\nexport function clearSentientEvents(): void {\n captured.length = 0;\n}\n\n/** True if any captured event is a goal (component goal_achieved or named goal) with this name. */\nexport function hasFiredGoal(events: CapturedEvent[], goalName: string): boolean {\n return events.some(\n (e) => (e.eventType === 'goal_achieved' || e.eventType === 'goal') && e.goalType === goalName,\n );\n}\n","import { recordEvent, type CapturedEvent } from './events.js';\nimport { confidenceBandOf, type SentientScenario, type ScenarioApiOverride } from './scenario.js';\n\n/** A framework-agnostic resolved response. `json` undefined ⇒ empty body. */\nexport type ResolvedResponse = { status: number; json?: unknown };\n\nfunction pathOf(url: string): string {\n try { return new URL(url).pathname; } catch { return url.split('?')[0] ?? url; }\n}\n\nasync function apiOverride(scenario: SentientScenario, route: string): Promise<ResolvedResponse | null> {\n const o: ScenarioApiOverride | undefined = scenario.api?.[route];\n if (o === undefined) return null;\n if (o === 'error') return { status: 500 };\n if (typeof o === 'number') return { status: o };\n if (o.delayMs) await new Promise((r) => setTimeout(r, o.delayMs));\n return { status: o.status ?? 200, json: o.body ?? {} };\n}\n\n/**\n * Resolve a request against a scenario. Returns a response, or null for routes\n * outside `/v1/*` (let the caller pass through). Shared by the MSW handlers and\n * the Playwright/Cypress adapters so behaviour can't drift.\n */\nexport async function resolveScenario(\n scenario: SentientScenario,\n _method: string,\n url: string,\n bodyText: string | null,\n): Promise<ResolvedResponse | null> {\n const path = pathOf(url);\n if (!path.includes('/v1/')) return null;\n\n const route = '/v1/' + (path.split('/v1/')[1] ?? '');\n const override = await apiOverride(scenario, route);\n if (override) return override;\n\n const body = bodyText ? (JSON.parse(bodyText) as unknown) : {};\n\n if (route === '/v1/sessions') return { status: 204 };\n\n if (route === '/v1/events') {\n for (const e of body as CapturedEvent[]) recordEvent(e);\n return { status: 204 };\n }\n\n if (route === '/v1/goals') {\n recordEvent({ eventType: 'goal', goalType: (body as { name?: string }).name });\n return { status: 204 };\n }\n\n if (route === '/v1/assign') {\n const b = body as { componentId: string; variantIds?: string[] };\n const variantId = scenario.variants?.[b.componentId] ?? b.variantIds?.[0] ?? 'control';\n return { status: 200, json: { variantId, assignmentTtlMs: 60_000 } };\n }\n\n if (route === '/v1/decide') {\n const b = body as {\n sections?: { id: string }[];\n slots?: Array<{\n id: string;\n arms?: string[];\n dims?: Record<string, string[]>;\n baseline?: string | Record<string, string>;\n }>;\n };\n const layoutOrder = scenario.layout ?? (b.sections ?? []).map((s) => s.id);\n const json: Record<string, unknown> = {\n layoutOrder,\n assignments: scenario.variants ?? {},\n persona: scenario.persona ?? 'unknown',\n confidence: scenario.confidence ?? 1,\n };\n // Mirror the real server: the slots key exists ONLY when slots were requested.\n if (b.slots && b.slots.length > 0) {\n const slots: Record<string, unknown> = {};\n for (const decl of b.slots) {\n slots[decl.id] = scenario.slots?.[decl.id] ?? defaultSlotResult(decl);\n }\n json.slots = slots;\n }\n return { status: 200, json };\n }\n\n if (route === '/v1/explain') {\n const b = body as {\n sections?: { id: string }[];\n persona?: string;\n slots?: Array<{\n id: string;\n arms?: string[];\n dims?: Record<string, string[]>;\n baseline?: string | Record<string, string>;\n }>;\n };\n const layoutOrder = scenario.layout ?? (b.sections ?? []).map((s) => s.id);\n const persona = b.persona ?? scenario.persona ?? 'unknown';\n const confidence = scenario.confidence ?? 1;\n const json: Record<string, unknown> = {\n layoutOrder,\n assignments: scenario.variants ?? {},\n persona,\n reasons: [],\n personaAttributes: { persona, confidence: confidenceBandOf(confidence) },\n };\n if (b.slots && b.slots.length > 0) {\n const slots: Record<string, unknown> = {};\n for (const decl of b.slots) {\n slots[decl.id] = scenario.slots?.[decl.id] ?? defaultSlotResult(decl);\n }\n json.slots = slots;\n }\n return { status: 200, json };\n }\n\n if (route === '/v1/weights') {\n const components = Object.entries(scenario.weights ?? {}).map(([componentId, variants]) => ({ componentId, updatedAt: 0, variants }));\n return { status: 200, json: { components } };\n }\n\n return null;\n}\n\n/** Declared baseline of a slot: explicit `baseline`, else first arm / first value per dim. */\nfunction defaultSlotResult(decl: {\n arms?: string[];\n dims?: Record<string, string[]>;\n baseline?: string | Record<string, string>;\n}): string | Record<string, string> {\n if (decl.arms) {\n return typeof decl.baseline === 'string' ? decl.baseline : decl.arms[0] ?? 'baseline';\n }\n const out: Record<string, string> = {};\n for (const [dim, values] of Object.entries(decl.dims ?? {})) {\n const declared =\n typeof decl.baseline === 'object' && decl.baseline !== null ? decl.baseline[dim] : undefined;\n out[dim] = declared ?? values[0] ?? '';\n }\n return out;\n}\n","import { resolveScenario } from './resolve.js';\nimport { getSentientEvents, type CapturedEvent } from './events.js';\nimport type { SentientScenario } from './scenario.js';\n\ntype InitData = {\n overrides: Record<string, string>;\n layout: string[] | null;\n slots: Record<string, string | Record<string, string>> | null;\n persona: string | null;\n confidence: number;\n};\n\ntype PwRoute = {\n request(): { method(): string; url(): string; postData(): string | null };\n fulfill(r: { status: number; contentType?: string; body?: string }): Promise<void>;\n continue(): Promise<void>;\n};\n\n/** Structural subset of Playwright's `Page` — avoids a hard dependency on @playwright/test. */\ntype PwPage = {\n // Return types are intentionally `Promise<unknown>`: the helper never uses\n // them, and pinning `Promise<void>` breaks against Playwright versions whose\n // `addInitScript`/`route` resolve to `Disposable` rather than `void`.\n addInitScript(script: (arg: InitData) => void, arg: InitData): Promise<unknown>;\n route(url: string, handler: (route: PwRoute) => unknown): Promise<unknown>;\n};\n\n/**\n * Make a Playwright `page` serve a SentientUI scenario: forces\n * variants/layout/slots/persona before load (including the persona html\n * attributes) and stubs every `/v1/*` request from the scenario, capturing\n * posted events. Returns a handle exposing `.events()`.\n */\nexport async function mockSentient(\n page: PwPage,\n scenario: SentientScenario = {},\n): Promise<{ events: () => CapturedEvent[] }> {\n const initData: InitData = {\n overrides: scenario.variants ?? {},\n layout: scenario.layout ?? null,\n slots: scenario.slots ?? null,\n persona: scenario.persona ?? null,\n confidence: scenario.confidence ?? 1,\n };\n await page.addInitScript((data: InitData) => {\n const w = window as unknown as Record<string, unknown>;\n w.__sentient_overrides = data.overrides;\n if (data.layout) w.__sentient_layout_override = data.layout;\n if (data.slots) w.__sentient_slot_overrides = data.slots;\n if (data.persona) {\n w.__sentient_persona_override = { persona: data.persona, confidence: data.confidence };\n try {\n // Inline banding: this function is SERIALIZED into the page context,\n // so it cannot close over imports. Cutoffs pinned to policy\n // confidenceBand (<0.3 low, <0.7 medium, else high).\n const band = data.confidence < 0.3 ? 'low' : data.confidence < 0.7 ? 'medium' : 'high';\n document.documentElement.setAttribute('data-sentient-persona', data.persona);\n document.documentElement.setAttribute('data-sentient-confidence', band);\n } catch {\n /* document not ready — the SDK adopts the override globals instead */\n }\n }\n }, initData);\n\n await page.route('**/v1/**', async (route) => {\n const req = route.request();\n const resolved = await resolveScenario(scenario, req.method(), req.url(), req.postData());\n if (!resolved) return route.continue();\n await route.fulfill({\n status: resolved.status,\n contentType: 'application/json',\n body: JSON.stringify(resolved.json ?? {}),\n });\n });\n\n return { events: () => getSentientEvents() };\n}\n","import { resolveScenario } from './resolve.js';\nimport { confidenceBandOf, type SentientScenario } from './scenario.js';\n\ntype CyReq = {\n method: string;\n url: string;\n body: unknown;\n reply(r: { statusCode: number; body?: unknown }): void;\n};\n\n/** Structural subset of Cypress's `cy` — avoids a hard dependency on cypress. */\ntype Cy = {\n intercept(url: string, handler: (req: CyReq) => void | Promise<void>): unknown;\n on(event: string, cb: (win: Record<string, unknown>) => void): unknown;\n};\n\n/**\n * Make Cypress serve a SentientUI scenario: forces variants/layout/slots/persona\n * on the app window before load (including the persona html attributes) and\n * stubs every `/v1/*` request from the scenario. Call in a `beforeEach` before\n * `cy.visit`.\n *\n * Note: for event assertions in Cypress, alias the intercept (`cy.intercept(...).as('ev')`)\n * and `cy.wait('@ev')` — captured module state does not cross the browser/Node boundary.\n */\nexport function mockSentientCypress(cy: Cy, scenario: SentientScenario = {}): void {\n const overrides = scenario.variants ?? {};\n const layout = scenario.layout ?? null;\n const slots = scenario.slots ?? null;\n const persona = scenario.persona ?? null;\n const confidence = scenario.confidence ?? 1;\n\n cy.on('window:before:load', (win) => {\n win.__sentient_overrides = overrides;\n if (layout) win.__sentient_layout_override = layout;\n if (slots) win.__sentient_slot_overrides = slots;\n if (persona) {\n win.__sentient_persona_override = { persona, confidence };\n try {\n const doc = (win as { document?: Document }).document;\n doc?.documentElement?.setAttribute('data-sentient-persona', persona);\n doc?.documentElement?.setAttribute('data-sentient-confidence', confidenceBandOf(confidence));\n } catch {\n /* document not ready — the SDK adopts the override globals instead */\n }\n }\n });\n\n cy.intercept('**/v1/**', async (req) => {\n const resolved = await resolveScenario(\n scenario,\n req.method,\n req.url,\n req.body != null ? JSON.stringify(req.body) : null,\n );\n if (!resolved) return; // passthrough\n req.reply({ statusCode: resolved.status, body: (resolved.json ?? '') as unknown });\n });\n}\n"],"mappings":"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","names":["render","createContext","useContext","useEffect","useMemo","useRef","useState","useSyncExternalStore","detectDeviceClass","detectTrafficSource","init","store","listeners","update","componentId","weights","store","set","listeners","cb","e","ssrFallback","state","w","getPreviewMode","state","subscribePreview","fn","listeners","createPreviewClient","inner","componentId","segment","variantIds","agentData","agentDataByVariant","slotId","EVENT","notifyOverridesChanged","_a","w","EVENT","publishDevtoolsConfig","config","isDevBuild","_a","ssrFallback","state","w","emit","fn","registerSections","sections","isDevBuild","state","emit","jsx","deriveDefaultSegment","_a","_b","device","detectDeviceClass","source","detectTrafficSource","e","DEFAULT_API_BASE_URL","AdaptiveContext","createContext","useConsentSource","granted","setGranted","useState","sourceRef","useRef","cookie","value","event","hasCheck","useEffect","read","s","want","c","onDecision","AdaptiveProvider","props","client","setClient","sessionSegment","previewOn","setPreviewOn","getPreviewMode","subscribePreview","sourceGranted","consent","prev","config","cancelled","created","stopEngagement","startEngagement","startEngagementCapture","initGraph","__spreadProps","__spreadValues","init","poll","entries","entry","weights","v","update","timerId","ssrFallback","apiBaseUrl","frozenConfigRef","current","key","publishDevtoolsConfig","decided","registerSections","exposedClient","useMemo","createPreviewClient","_c","_d","_e","confidenceBandOf","c","applyScenario","scenario","_a","_b","_c","w","__spreadValues","d","e","notifyOverridesChanged","resetScenario","http","HttpResponse","captured","recordEvent","getSentientEvents","clearSentientEvents","hasFiredGoal","events","goalName","e","pathOf","url","_a","e","apiOverride","scenario","route","_b","_c","o","r","resolveScenario","_method","bodyText","_d","_e","_f","_g","_h","_i","_j","_k","_l","_m","_n","_o","_p","_q","_r","_s","_t","_u","path","override","body","recordEvent","b","json","s","slots","decl","defaultSlotResult","layoutOrder","persona","confidence","confidenceBandOf","componentId","variants","out","dim","values","declared","scenarioToHandlers","scenario","http","request","bodyText","r","resolveScenario","HttpResponse","mockSentient","page","scenario","_a","_b","_c","_d","_e","initData","data","w","band","e","route","req","resolved","resolveScenario","getSentientEvents","mockSentientCypress","cy","scenario","_a","_b","_c","_d","_e","overrides","layout","slots","persona","confidence","win","doc","confidenceBandOf","e","req","resolved","resolveScenario","jsx","renderWithSentient","ui","scenario","options","applyScenario","render","jsx","AdaptiveProvider","setupSentientTests","_a","g","resetScenario"]}
1
+ {"version":3,"sources":["../src/testing/index.tsx","../src/provider.tsx","../src/weights-store.ts","../src/preview-mode.ts","../src/override-events.ts","../src/devtools-config.ts","../src/adaptive-shared.ts","../src/devtools-registry.ts","../src/testing/scenario.ts","../src/testing/handlers.ts","../src/testing/events.ts","../src/testing/resolve.ts","../src/testing/playwright.ts","../src/testing/cypress.ts"],"sourcesContent":["import type { ReactElement } from 'react';\nimport { render, type RenderOptions, type RenderResult } from '@testing-library/react';\nimport { AdaptiveProvider } from '../provider.js';\nimport { applyScenario, resetScenario, type SentientScenario } from './scenario.js';\n\nexport type { SentientScenario };\nexport { applyScenario, resetScenario };\nexport { scenarioToHandlers } from './handlers.js';\nexport { resolveScenario, type ResolvedResponse } from './resolve.js';\nexport { getSentientEvents, clearSentientEvents, hasFiredGoal, type CapturedEvent } from './events.js';\nexport type { ScenarioWeight, ScenarioApiOverride } from './scenario.js';\nexport { mockSentient } from './playwright.js';\nexport { mockSentientCypress } from './cypress.js';\n\n/**\n * Render `ui` under a SentientUI provider configured for tests: consent is off,\n * so the SDK never initialises a client and every <Adaptive> renders its control\n * variant with zero network. A scenario forces specific variants/layout.\n */\nexport function renderWithSentient(\n ui: ReactElement,\n scenario: SentientScenario = {},\n options?: RenderOptions,\n): RenderResult {\n applyScenario(scenario);\n return render(\n <AdaptiveProvider apiKey=\"pk_test\" context=\"saas\" consent={false}>\n {ui}\n </AdaptiveProvider>,\n options,\n );\n}\n\n/** Call in a test setup file to reset forced state after each test. */\nexport function setupSentientTests(): void {\n const g = globalThis as unknown as { afterEach?: (fn: () => void) => void };\n g.afterEach?.(() => resetScenario());\n}\n","'use client';\n\ndeclare const process: { env?: { NODE_ENV?: string } } | undefined;\n\nimport {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n useSyncExternalStore,\n type ReactNode,\n} from 'react';\nimport {\n detectDeviceClass,\n detectTrafficSource,\n init,\n type SentientClient,\n type SentientConfig,\n type SlotResult,\n} from '@sentientui/core';\nimport { update as updateWeightsStore, type ComponentWeights } from './weights-store.js';\nimport { getPreviewMode, subscribePreview, createPreviewClient } from './preview-mode.js';\nimport { subscribeOverridesChanged } from './override-events.js';\nimport { publishDevtoolsConfig } from './devtools-config.js';\nimport { registerSections } from './devtools-registry.js';\n\n/**\n * Mirrors the segment derivation inside core `init()` so the cache key used\n * by `useAssignment` always matches the key `assign()` writes under. Before\n * this, the context defaulted to 'desktop:direct' while core used the\n * detected segment — a systematic cache miss for every integration that\n * didn't pass `sessionSegment` explicitly.\n */\nfunction deriveDefaultSegment(): string {\n if (typeof window === 'undefined') return 'desktop:direct';\n try {\n const device = detectDeviceClass(navigator.userAgent ?? '');\n const source = detectTrafficSource(document.referrer ?? '', window.location.origin);\n return `${device}:${source}`;\n } catch {\n return 'desktop:direct';\n }\n}\n\nconst DEFAULT_API_BASE_URL = 'https://api.sentient-ui.com/v1';\n\n/** How to render adaptive slots during SSR when assignments are not preloaded. */\nexport type SsrFallback = 'first' | 'none';\n\ntype AdaptiveContextValue = {\n client: SentientClient | null;\n // The publishable API key (pk_…). Historically named `projectId` because the\n // API uses it as the project identifier on the wire, but it is the API key,\n // not the project UUID. Field renamed for clarity.\n apiKey: string;\n initialAssignments: Record<string, string>;\n sessionSegment: string;\n ssrFallback: SsrFallback;\n onAssignment: ((componentId: string, variantId: string) => void) | undefined;\n initialLayoutOrder: string[] | null;\n initialSlots: Record<string, SlotResult>;\n initialPersona: { persona: string; confidence: number } | null;\n apiBaseUrl: string;\n debug: boolean;\n};\n\nconst AdaptiveContext = createContext<AdaptiveContextValue>({\n client: null,\n apiKey: '',\n initialAssignments: {},\n sessionSegment: 'desktop:direct',\n ssrFallback: 'first',\n onAssignment: undefined,\n initialLayoutOrder: null,\n initialSlots: {},\n initialPersona: null,\n apiBaseUrl: DEFAULT_API_BASE_URL,\n debug: false,\n});\n\nexport type AdaptiveProviderProps = {\n apiKey: string;\n context: SentientConfig['context'];\n debug?: boolean;\n /**\n * SSR-preloaded assignments from `preloadAssignments()` / `loadAdaptiveAssignments()`.\n * Passed through to `useAssignment` as synchronous initial state so crawlers and\n * the first paint see real content (recommended for SEO).\n */\n initialAssignments?: Record<string, string>;\n /**\n * Bandit segment from SSR (`device:source`). Keeps cache, assign, and worker\n * weights on one row — must match `loadAdaptiveAssignments` / session upsert.\n */\n sessionSegment?: string;\n /**\n * When no `initialAssignments` exist for a component, `'first'` renders\n * `variantIds[0]` in server HTML (safe default for SEO). Use `'none'` only for\n * decorative slots marked `clientOnly`.\n * @default 'first'\n */\n ssrFallback?: SsrFallback;\n /**\n * Consent gate. When `false` the SDK is not initialised and no events are\n * sent. Flip to `true` (e.g. after the user accepts the cookie banner) to\n * initialise and begin tracking.\n */\n consent?: boolean;\n /**\n * Where to read the visitor's consent decision from, so the SDK can gate and\n * un-gate itself instead of the host app wiring `grantConsent()` by hand.\n *\n * Keep your own banner or CMP — this only tells us how to observe it. The\n * provider reads the source on mount, re-reads it whenever `event` fires, and\n * initialises the moment it grants. Nothing is requested and no cookie is set\n * until then, and there is no page reload.\n *\n * Because the provider owns the whole lifecycle there is no ordering rule to\n * get right: pass this instead of managing `consent` yourself.\n *\n * @example // cookie written by your own banner\n * consentFrom={{ cookie: 'cookie_consent', value: 'accepted', event: 'consent-decided' }}\n * @example // a CMP that exposes an object rather than a cookie\n * consentFrom={{ check: () => window.Cookiebot?.consent?.statistics === true,\n * event: 'CookiebotOnAccept' }}\n */\n consentFrom?: {\n /** Cookie to read. Granted when its value equals `value`. */\n cookie?: string;\n /** Cookie value that means granted. @default 'accepted' */\n value?: string;\n /** Predicate for CMPs with a JS API. Takes precedence over `cookie`. */\n check?: () => boolean;\n /**\n * `window` event that signals a decision. The source is re-read when it\n * fires — the event's payload is never trusted — so any CMP's event works.\n */\n event?: string;\n };\n /**\n * Behavior before consent is granted. Pass `'statistical_winner'` to serve the\n * best-performing variant via `GET /v1/winner` with zero tracking while the\n * consent banner is showing. Requires `consent: false`.\n * @see SentientConfig.preConsentBehavior\n */\n preConsentBehavior?: 'statistical_winner' | 'control';\n /**\n * Honor the browser's Do Not Track signal. Defaults to `true`: when DNT is\n * enabled the SDK sets no cookies and sends no tracking data (overriding\n * `consent: true`). Set `false` to make your own consent gate authoritative.\n * @see SentientConfig.respectDoNotTrack\n */\n respectDoNotTrack?: boolean;\n /**\n * Called once per component the first time a variant is resolved for that\n * component in this session. Use to forward assignments to your own analytics\n * (Mixpanel, PostHog, Segment, etc.) without having to wrap `useAssignment`.\n */\n onAssignment?: (componentId: string, variantId: string) => void;\n /**\n * SSR-preloaded section order from `loadAdaptiveDecision()`.\n * Pass the `layoutOrder` field from `DecideResult`. When set,\n * `useLayoutOrder()` returns this on first render so there is no layout shift.\n */\n initialLayoutOrder?: string[] | null;\n /**\n * The section ids the app declares as reorderable, independent of any\n * decision. `AdaptiveRoot` forwards its `sections` prop here.\n *\n * Devtools reads this to offer layout previewing: a page whose decision was\n * gated by consent, or timed out, has no `initialLayoutOrder`, and registering\n * only that left the layout panel empty in exactly the situation you reach for\n * it — running the site locally before accepting a cookie banner.\n */\n declaredSections?: string[];\n /**\n * SSR-preloaded slot results from `loadAdaptiveDecision()` (the `slots`\n * field of its result). Guarantees `useAdaptiveTokens`/`AdaptiveGroup`\n * render the decided arm in server HTML — zero flicker, hydration-safe.\n */\n initialSlots?: Record<string, SlotResult>;\n /**\n * Persona decided during SSR (`persona` + `confidence` fields of\n * `loadAdaptiveDecision()`'s result). Adopted by the core client;\n * rendered into html attributes only by `SentientPersonaScript`.\n */\n initialPersona?: { persona: string; confidence: number };\n /**\n * Base URL of the Sentient API (no trailing slash). Read by the devtools\n * panel for /v1/explain and by future client helpers. Defaults to the\n * hosted API.\n */\n apiBaseUrl?: string;\n /**\n * Session ID generated during SSR (the `sessionId` field returned by\n * `loadAdaptiveAssignments` / `loadAdaptiveDecision`). When provided and no\n * existing session cookie or localStorage entry is found, the client adopts\n * this ID so events and goals are attributed to the same session the server\n * used for variant assignment.\n */\n ssrSessionId?: string;\n /**\n * ISO 3166-1 alpha-2 country code. Pass the value of the `CF-IPCountry`\n * header from your Next.js server component to populate country on landing\n * sessions without client-side geo lookup.\n */\n country?: string;\n /**\n * Keyless local mode. 'auto' (default) simulates decisions on-device in\n * development builds when no valid API key is configured; `true` forces the\n * local engine; `false` restores the silent keyless no-op.\n * @see SentientConfig.localMode\n */\n localMode?: 'auto' | boolean;\n /**\n * DOM graph scanning + page-structure sync. ON by default: the provider\n * dynamically loads `@sentientui/core/graph` and uses its graph-capable\n * `init()` for the single client, so the SDK scans your page structure,\n * auto-detects semantic sections, and syncs them to power personas and the\n * dashboard graph page. Pass `false` to keep the lean bundle only.\n */\n enableGraph?: boolean;\n /**\n * Include captured heading / DOM text in graph sync payloads. OFF by default.\n * Only applies when graph scanning is enabled (`enableGraph` not `false`).\n */\n captureDomText?: boolean;\n /**\n * Behavioral engagement capture (per-section dwell/scroll + semantic section\n * registration) powering personas. ON by default — pass `false` to disable.\n * Never runs for a DNT/GPC or consent-gated visitor (no client → no capture).\n */\n engagement?: boolean;\n children: ReactNode;\n};\n\n/**\n * Watches a {@link AdaptiveProviderProps.consentFrom} source and reports whether\n * it currently grants consent. Returns false (and subscribes to nothing) when no\n * source is configured.\n */\nfunction useConsentSource(source: AdaptiveProviderProps['consentFrom']): boolean {\n const [granted, setGranted] = useState(false);\n\n // The source is usually an inline object literal, so its identity changes\n // every render. Read it through a ref and key the effect on its primitives,\n // otherwise the listener would be torn down and re-added on every render.\n const sourceRef = useRef(source);\n sourceRef.current = source;\n\n const { cookie, value, event } = source ?? {};\n const hasCheck = typeof source?.check === 'function';\n\n useEffect(() => {\n const read = (): boolean => {\n const s = sourceRef.current;\n if (!s) return false;\n if (s.check) return s.check() === true;\n if (!s.cookie || typeof document === 'undefined') return false;\n const want = `${s.cookie}=${s.value ?? 'accepted'}`;\n return document.cookie.split('; ').some((c) => c.trim() === want);\n };\n\n if (read()) {\n setGranted(true);\n return;\n }\n if (!event) return;\n\n // Re-read rather than trusting the event payload, so this works with any\n // CMP's event shape (CookiebotOnAccept, OneTrustGroupsUpdated, …) and a\n // \"declined\" decision correctly leaves us gated.\n const onDecision = (): void => {\n if (read()) setGranted(true);\n };\n window.addEventListener(event, onDecision);\n return () => window.removeEventListener(event, onDecision);\n }, [cookie, value, event, hasCheck]);\n\n return granted;\n}\n\n/**\n * Initialises the Sentient core SDK in a useEffect (SSR-safe) and exposes the\n * client via React context. Re-initialises when consent changes.\n */\nexport function AdaptiveProvider(props: AdaptiveProviderProps): JSX.Element {\n const [client, setClient] = useState<SentientClient | null>(null);\n // Derived once per mount: identical to what core init() computes, so cache\n // reads (context segment) and cache writes (core segment) always agree.\n const [sessionSegment] = useState(() => props.sessionSegment ?? deriveDefaultSegment());\n // Devtools preview: when on, expose an event-suppressing client so previewing\n // variants/personas writes nothing. Off by default (inert in production).\n const [previewOn, setPreviewOn] = useState(getPreviewMode());\n useEffect(() => subscribePreview(() => setPreviewOn(getPreviewMode())), []);\n\n // When a consentFrom source is configured it owns the gate: start closed and\n // open only once the source grants. An explicit consent={true} (e.g. resolved\n // from the cookie on the server by AdaptiveRoot) short-circuits it, so a\n // returning visitor isn't gated waiting for a client-side re-read.\n const sourceGranted = useConsentSource(props.consentFrom);\n const consent = props.consentFrom ? props.consent === true || sourceGranted : props.consent;\n\n useEffect(() => {\n // When consent is explicitly false with no preConsentBehavior, tear down any existing client.\n if (consent === false && !props.preConsentBehavior) {\n setClient((prev: SentientClient | null) => {\n prev?.destroy();\n return null;\n });\n return;\n }\n\n const config = {\n apiKey: props.apiKey,\n context: props.context,\n debug: props.debug,\n initialAssignments: props.initialAssignments,\n sessionSegment,\n consent,\n preConsentBehavior: props.preConsentBehavior,\n respectDoNotTrack: props.respectDoNotTrack,\n ssrSessionId: props.ssrSessionId,\n country: props.country,\n localMode: props.localMode,\n initialSlots: props.initialSlots,\n initialPersona: props.initialPersona,\n ingestUrl: props.apiBaseUrl ? `${props.apiBaseUrl.replace(/\\/$/, '')}/events` : undefined,\n };\n\n // Track the client created by this effect run so cleanup destroys exactly\n // the right one, and so a late-resolving dynamic import can bail if the\n // effect was already torn down (unmount / consent change).\n let cancelled = false;\n let created: SentientClient | null = null;\n let stopEngagement: (() => void) | null = null;\n\n // Engagement capture (default ON): per-section dwell/scroll + semantic\n // section registration, lazy-loaded so the lean bundle stays lean. Only\n // ever started for an initialised client, so consent/DNT gates are\n // inherited (and the capture module re-checks DNT internally).\n const startEngagement = (c: SentientClient): void => {\n if (props.engagement === false) return;\n void import('@sentientui/core/engagement').then(({ startEngagementCapture }) => {\n if (cancelled) return;\n stopEngagement = startEngagementCapture(c, {\n apiKey: props.apiKey,\n apiBase: props.apiBaseUrl ? props.apiBaseUrl.replace(/\\/$/, '') : undefined,\n });\n });\n };\n\n if (props.enableGraph !== false) {\n // Graph scanning is the default — load the graph entry dynamically so the\n // scanner never lands in the lean bundle. The provider still creates ONE\n // client (graph-capable). Pass enableGraph={false} for the lean client.\n void import('@sentientui/core/graph').then(({ init: initGraph }) => {\n if (cancelled) return;\n created = initGraph({ ...config, graph: true, captureDomText: props.captureDomText === true });\n setClient(created);\n startEngagement(created);\n });\n } else {\n created = init(config);\n setClient(created);\n startEngagement(created);\n }\n\n return () => {\n cancelled = true;\n stopEngagement?.();\n // dispose, not destroy: effect cleanup runs on unmount, StrictMode's\n // dev double-invoke, and consent re-init — the visitor identity must\n // survive all of those. Full destroy() happens only on the explicit\n // consent-revocation branch above.\n created?.dispose();\n };\n // Re-init when consent changes — whether that came from the prop or from a\n // consentFrom source granting. Other props (incl. enableGraph) are\n // intentionally stable for a session.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [consent]);\n\n // Poll /v1/weights every 60 s so long-lived sessions see updated bandit weights\n // without a page reload. useAssignment subscribers react via weights-store.\n useEffect(() => {\n if (!client) return;\n let cancelled = false;\n const poll = async (): Promise<void> => {\n if (cancelled) return;\n let entries;\n try {\n entries = await client.fetchWeights();\n } catch {\n // Network/transient error: skip this cycle and retry on the next\n // interval. Swallowed deliberately so a failed poll never surfaces as\n // an unhandled rejection.\n return;\n }\n if (cancelled) return;\n for (const entry of entries) {\n const weights: ComponentWeights = {\n componentId: entry.componentId,\n updatedAt: entry.updatedAt,\n variants: entry.variants.map((v) => ({\n variantId: v.variantId,\n pulls: v.pulls,\n avgReward: v.avgReward ?? 0,\n })),\n };\n updateWeightsStore(entry.componentId, weights);\n }\n };\n void poll();\n const timerId = setInterval(() => void poll(), 60_000);\n return () => {\n cancelled = true;\n clearInterval(timerId);\n };\n }, [client]);\n\n const ssrFallback = props.ssrFallback ?? 'first';\n // Strip a trailing slash so consumers (devtools /explain, useAdaptiveApiBaseUrl)\n // build URLs the same way the core client does (it strips before appending\n // /events and /section-map) — otherwise an apiBaseUrl ending in \"/\" yields a\n // double slash like \".../v1//explain\".\n const apiBaseUrl = (props.apiBaseUrl ?? DEFAULT_API_BASE_URL).replace(/\\/$/, '');\n\n // The init effect above re-runs only on `props.consent`: apiKey / context /\n // country / apiBaseUrl are captured once and are deliberately stable for the\n // session, so changing them at runtime silently no-ops. That silence is\n // surprising — warn (dev only) when one actually changes value after init, so\n // the no-op is visible. To apply a new value, remount the provider (e.g. a\n // changing React `key`).\n const frozenConfigRef = useRef<{\n apiKey: string;\n context: SentientConfig['context'];\n country: string | undefined;\n apiBaseUrl: string;\n } | null>(null);\n useEffect(() => {\n if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'production') return;\n const current = { apiKey: props.apiKey, context: props.context, country: props.country, apiBaseUrl };\n const prev = frozenConfigRef.current;\n frozenConfigRef.current = current;\n if (prev === null) return; // first run: capture the frozen baseline, nothing to compare\n for (const key of ['apiKey', 'context', 'country', 'apiBaseUrl'] as const) {\n if (!Object.is(prev[key], current[key])) {\n console.warn(\n `[sentient] AdaptiveProvider: \\`${key}\\` changed after initialisation, but the SDK client is stable for the session and only re-inits on \\`consent\\` — the new value is ignored. Remount the provider (e.g. via a changing \\`key\\` prop) to apply it.`,\n );\n }\n }\n }, [props.apiKey, props.context, props.country, apiBaseUrl]);\n\n // Publish devtools config through window: the /devtools entry is a separate\n // bundle and cannot read this provider's context instance. Dev-only.\n useEffect(() => {\n if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'production') return;\n publishDevtoolsConfig({\n apiKey: props.apiKey,\n apiBaseUrl,\n isLocal: client?.isLocal === true,\n });\n }, [client, props.apiKey, apiBaseUrl]);\n\n // Sections registry for devtools /v1/explain + local simulation. The decided\n // order wins when there is one (it is what the page is actually rendering);\n // the declared list is the fallback so the layout panel still knows what is\n // reorderable when no decision arrived.\n useEffect(() => {\n const decided = props.initialLayoutOrder;\n if (decided && decided.length > 0) {\n registerSections(decided);\n return;\n }\n if (props.declaredSections && props.declaredSections.length > 0) {\n registerSections(props.declaredSections);\n }\n }, [props.initialLayoutOrder, props.declaredSections]);\n\n // The client exposed to consumers — wrapped to suppress events while previewing.\n const exposedClient = useMemo(\n () => (client && previewOn ? createPreviewClient(client) : client),\n [client, previewOn],\n );\n\n // Memoized so unrelated parent re-renders don't cascade through every\n // useSentient / useAssignment consumer via a fresh context object.\n const value = useMemo<AdaptiveContextValue>(\n () => ({\n client: exposedClient,\n apiKey: props.apiKey,\n initialAssignments: props.initialAssignments ?? {},\n sessionSegment,\n ssrFallback,\n onAssignment: props.onAssignment,\n initialLayoutOrder: props.initialLayoutOrder ?? null,\n initialSlots: props.initialSlots ?? {},\n initialPersona: props.initialPersona ?? null,\n apiBaseUrl,\n debug: props.debug ?? false,\n }),\n [\n exposedClient,\n props.apiKey,\n props.initialAssignments,\n sessionSegment,\n ssrFallback,\n props.onAssignment,\n props.initialLayoutOrder,\n props.initialSlots,\n props.initialPersona,\n apiBaseUrl,\n props.debug,\n ],\n );\n\n return (\n <AdaptiveContext.Provider value={value}>\n {props.children}\n </AdaptiveContext.Provider>\n );\n}\n\n/**\n * Returns the SentientClient, or null until the provider has finished\n * initialising on the client.\n */\nexport function useSentient(): SentientClient | null {\n return useContext(AdaptiveContext).client;\n}\n\n/** Internal: publishable API key carried alongside the client. */\nexport function useAdaptiveApiKey(): string {\n return useContext(AdaptiveContext).apiKey;\n}\n\n/** Internal: SSR-preloaded assignments for hydration-safe first render. */\nexport function useInitialAssignments(): Record<string, string> {\n return useContext(AdaptiveContext).initialAssignments;\n}\n\n/** Internal: bandit segment aligned with SSR session upsert. */\nexport function useSessionSegment(): string {\n return useContext(AdaptiveContext).sessionSegment;\n}\n\n/** Internal: SSR fallback strategy when a slot has no preloaded assignment. */\nexport function useSsrFallback(): SsrFallback {\n return useContext(AdaptiveContext).ssrFallback;\n}\n\n/** Internal: forwarding hook for consumer analytics integration. */\nexport function useOnAssignment(): ((componentId: string, variantId: string) => void) | undefined {\n return useContext(AdaptiveContext).onAssignment;\n}\n\n/** Internal: debug flag from the provider config, for dev-only diagnostic logging. */\nexport function useDebug(): boolean {\n return useContext(AdaptiveContext).debug;\n}\n\n/**\n * Returns the persona-specific section order from SSR, or null when no\n * sections were declared on AdaptiveRoot or reliability is below threshold.\n * Devtools/testing can force it via `window.__sentient_layout_override`;\n * consumers re-render when the devtools notifies an override change.\n */\nexport function useLayoutOrder(): string[] | null {\n const contextOrder = useContext(AdaptiveContext).initialLayoutOrder;\n const override = useSyncExternalStore(\n subscribeOverridesChanged,\n () =>\n typeof window === 'undefined'\n ? null\n : ((window as unknown as { __sentient_layout_override?: string[] })\n .__sentient_layout_override ?? null),\n () => null,\n );\n return override ?? contextOrder;\n}\n\n/** Internal: SSR-preloaded slot results for hydration-safe first render. */\nexport function useInitialSlots(): Record<string, SlotResult> {\n return useContext(AdaptiveContext).initialSlots;\n}\n\n/** Internal: SSR-decided persona carried alongside the client. */\nexport function useInitialPersona(): { persona: string; confidence: number } | null {\n return useContext(AdaptiveContext).initialPersona;\n}\n\n/** Internal: configured API base URL (devtools fetches /v1/explain against this, never a relative URL). */\nexport function useAdaptiveApiBaseUrl(): string {\n return useContext(AdaptiveContext).apiBaseUrl;\n}\n","/** Per-component weights store with isolated subscriptions. */\n\nexport type VariantWeight = {\n variantId: string;\n pulls: number;\n avgReward: number;\n};\n\nexport type ComponentWeights = {\n componentId: string;\n variants: VariantWeight[];\n updatedAt: number;\n};\n\ntype Listener = (weights: ComponentWeights) => void;\n\nconst store = new Map<string, ComponentWeights>();\nconst listeners = new Map<string, Set<Listener>>();\n\n/**\n * Subscribes a listener to a single component. Returns an unsubscribe function.\n * Updates to other components never trigger this listener.\n */\nexport function subscribe(componentId: string, cb: Listener): () => void {\n let set = listeners.get(componentId);\n if (!set) {\n set = new Set();\n listeners.set(componentId, set);\n }\n set.add(cb);\n return () => {\n set!.delete(cb);\n if (set!.size === 0) listeners.delete(componentId);\n };\n}\n\n/**\n * Replaces the weights for a component and notifies only that component's\n * subscribers.\n */\nexport function update(componentId: string, weights: ComponentWeights): void {\n store.set(componentId, weights);\n const set = listeners.get(componentId);\n if (!set) return;\n for (const cb of set) {\n try {\n cb(weights);\n } catch {\n /* never throw to other listeners */\n }\n }\n}\n\n/**\n * Returns the latest known weights for a component, or null if none seen.\n */\nexport function getWeights(componentId: string): ComponentWeights | null {\n return store.get(componentId) ?? null;\n}\n\n/** Test-only: wipe the entire store. */\nexport function _resetWeightsStore(): void {\n store.clear();\n listeners.clear();\n}\n","import type { SentientClient } from '@sentientui/core';\n\ntype PreviewState = { on: boolean; listeners: Set<() => void> };\nconst ssrFallback: PreviewState = { on: false, listeners: new Set() };\n\n// Window-backed: the /devtools entry (a separate bundle) toggles preview mode\n// and the provider (main bundle) must observe it.\nfunction state(): PreviewState {\n if (typeof window === 'undefined') return ssrFallback;\n const w = window as unknown as { __sentient_preview?: PreviewState };\n if (!w.__sentient_preview) w.__sentient_preview = { on: false, listeners: new Set() };\n return w.__sentient_preview;\n}\n\nexport function setPreviewMode(on: boolean): void {\n const s = state();\n if (s.on === on) return;\n s.on = on;\n for (const fn of s.listeners) fn();\n}\n\nexport function getPreviewMode(): boolean {\n return state().on;\n}\n\nexport function subscribePreview(fn: () => void): () => void {\n const listeners = state().listeners;\n listeners.add(fn);\n return () => {\n listeners.delete(fn);\n };\n}\n\n/**\n * Wraps a client so it writes nothing: reads pass through, all emitters no-op.\n * Used while previewing variants/personas so no `variant_assigned`, goal, or\n * session events are sent.\n */\nexport function createPreviewClient(inner: SentientClient): SentientClient {\n return {\n isLocal: inner.isLocal,\n track: () => undefined,\n goal: () => undefined,\n componentGoal: () => undefined,\n identify: () => undefined,\n fetchWeights: () => Promise.resolve([]),\n getAssignment: (componentId, segment) => inner.getAssignment(componentId, segment),\n assign: (componentId, variantIds, agentData, agentDataByVariant) =>\n inner.assign(componentId, variantIds, agentData, agentDataByVariant),\n // Reads pass through; decide is a write (slot decisions persist server-side)\n // so preview mode never issues it.\n decide: () => Promise.resolve(null),\n getSlotResult: (slotId) => inner.getSlotResult(slotId),\n getPersona: () => inner.getPersona(),\n getGraph: () => inner.getGraph(),\n dispose: () => inner.dispose(),\n destroy: () => inner.destroy(),\n };\n}\n","/**\n * Cross-bundle re-render bus for devtools overrides. The main entry and the\n * /devtools entry are separate bundles, so state and notifications go through\n * window (a version counter + a DOM event) — never module-local state.\n */\nconst EVENT = 'sentient:overrides-changed';\n\ntype VersionWindow = Window & { __sentient_overrides_version?: number };\n\nexport function getOverridesVersion(): number {\n if (typeof window === 'undefined') return 0;\n return (window as VersionWindow).__sentient_overrides_version ?? 0;\n}\n\nexport function notifyOverridesChanged(): void {\n if (typeof window === 'undefined') return;\n const w = window as VersionWindow;\n w.__sentient_overrides_version = (w.__sentient_overrides_version ?? 0) + 1;\n window.dispatchEvent(new Event(EVENT));\n}\n\nexport function subscribeOverridesChanged(fn: () => void): () => void {\n if (typeof window === 'undefined') return () => undefined;\n window.addEventListener(EVENT, fn);\n return () => window.removeEventListener(EVENT, fn);\n}\n","/** Provider → devtools config handoff. Window-backed: the /devtools entry is a\n * separate bundle and cannot share the provider's React context instance. */\nexport type DevtoolsConfig = {\n apiKey: string;\n apiBaseUrl: string;\n isLocal: boolean;\n};\n\ntype ConfigWindow = Window & { __sentient_devtools_config?: DevtoolsConfig };\n\nexport function publishDevtoolsConfig(config: DevtoolsConfig): void {\n if (typeof window === 'undefined') return;\n (window as ConfigWindow).__sentient_devtools_config = config;\n}\n\nexport function readDevtoolsConfig(): DevtoolsConfig | null {\n if (typeof window === 'undefined') return null;\n return (window as ConfigWindow).__sentient_devtools_config ?? null;\n}\n","import type { SentientClient } from '@sentientui/core';\n\ndeclare const process: { env?: { NODE_ENV?: string } } | undefined;\n\n/**\n * True unless NODE_ENV is 'production'. Never `process?.env` — optional\n * chaining still throws ReferenceError on an undeclared global, and browsers\n * without a bundler shim (raw esbuild, vanilla script tags) have no `process`.\n */\nexport function isDevBuild(): boolean {\n return typeof process === 'undefined' || process.env?.NODE_ENV !== 'production';\n}\n\nexport type ScrollDepthGoal = { type: 'scroll_depth'; threshold: number; value?: number };\nexport type ClickGoal = { type: 'click'; selector?: string; value?: number };\nexport type FormSubmitGoal = { type: 'form_submit'; value?: number };\nexport type CompositeGoal = { type: 'composite'; all: GoalConfig[] };\nexport type WeightedStep = { goal: GoalConfig; name: string; weight: number };\nexport type WeightedCompositeGoal = { type: 'weighted_composite'; steps: WeightedStep[] };\nexport type GoalConfig = ScrollDepthGoal | ClickGoal | FormSubmitGoal | CompositeGoal | WeightedCompositeGoal;\n\nexport function normalizeGoal(goal: string | GoalConfig): GoalConfig {\n if (typeof goal === 'string') return { type: 'click' };\n return goal;\n}\n\n/** The goalType label events are recorded under (named goal or config type). */\nexport function goalLabelOf(goal: string | GoalConfig): string {\n return typeof goal === 'string' ? goal : goal.type;\n}\n\n/** Static revenue value declared on a simple goal config, if any. Composites\n * carry no value (weights and values don't mix — spec §9.4); dynamic values\n * go through the imperative hooks. */\nexport function goalValueOf(goal: string | GoalConfig): number | undefined {\n if (typeof goal === 'string') return undefined;\n if (goal.type === 'click' || goal.type === 'form_submit' || goal.type === 'scroll_depth') {\n return goal.value;\n }\n return undefined;\n}\n\nfunction isClickableTarget(el: EventTarget | null): boolean {\n if (!(el instanceof Element)) return false;\n const tag = el.tagName.toLowerCase();\n if (tag === 'a' || tag === 'button') return true;\n const role = el.getAttribute('role');\n return role === 'button';\n}\n\nfunction findClickable(start: Element, container: Element, selector?: string): boolean {\n if (selector) {\n try {\n let cursor: Element | null = start;\n while (cursor && cursor !== container) {\n if (cursor.matches(selector)) return true;\n cursor = cursor.parentElement;\n }\n } catch {\n // Invalid CSS selector — treat as no match rather than breaking all click handlers.\n }\n return false;\n }\n let cursor: Element | null = start;\n while (cursor && cursor !== container) {\n if (isClickableTarget(cursor)) return true;\n cursor = cursor.parentElement;\n }\n return false;\n}\n\nexport type GoalHandlers = {\n /** Primary goal completion. Latch-once semantics are the CALLER's job. */\n fireGoal: () => void;\n /** Weighted-composite step completion (already deduped per step here). */\n fireStep: (name: string, weight: number, stepIndex: number) => void;\n};\n\n/**\n * Attaches the goal-detection listeners `<Adaptive>`'s container uses —\n * click / form_submit / scroll_depth, composite (all-of), and\n * weighted_composite (independent steps). Returns the cleanup function.\n * Extracted from adaptive.tsx so useAdaptive / useAdaptiveTokens /\n * AdaptiveGroup wire the SAME machinery instead of duplicating it.\n */\nexport function attachGoalListeners(node: Element, goal: GoalConfig, handlers: GoalHandlers): () => void {\n // --- Weighted composite: each step fires independently as it completes ---\n if (goal.type === 'weighted_composite') {\n const firedSteps = new Set<number>();\n const wcCleanups: Array<() => void> = [];\n\n goal.steps.forEach(({ goal: sub, name: stepName, weight: stepWeight }, idx) => {\n const fireStep = (): void => {\n if (firedSteps.has(idx)) return;\n firedSteps.add(idx);\n handlers.fireStep(stepName, stepWeight, idx);\n };\n\n if (sub.type === 'click') {\n const onClick = (e: Event): void => {\n const target = e.target;\n if (!(target instanceof Element)) return;\n if (!findClickable(target, node, sub.selector)) return;\n fireStep();\n };\n node.addEventListener('click', onClick);\n wcCleanups.push(() => node.removeEventListener('click', onClick));\n return;\n }\n\n if (sub.type === 'form_submit') {\n const onSubmit = (e: Event): void => {\n if (!(e.target instanceof HTMLFormElement)) return;\n if (!node.contains(e.target)) return;\n fireStep();\n };\n node.addEventListener('submit', onSubmit);\n wcCleanups.push(() => node.removeEventListener('submit', onSubmit));\n return;\n }\n\n if (sub.type === 'scroll_depth') {\n const threshold = Math.max(0, Math.min(1, sub.threshold));\n const io = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.intersectionRatio >= threshold) {\n fireStep();\n io.disconnect();\n break;\n }\n }\n },\n { threshold: [threshold] },\n );\n io.observe(node);\n wcCleanups.push(() => io.disconnect());\n }\n });\n\n return () => {\n for (const c of wcCleanups) c();\n };\n }\n // --- End weighted composite ---\n\n const subgoals: GoalConfig[] = goal.type === 'composite' ? goal.all : [goal];\n const remaining = new Set<number>(subgoals.map((_, i) => i));\n const checkComposite = (idx: number): void => {\n remaining.delete(idx);\n if (remaining.size === 0) handlers.fireGoal();\n };\n\n const cleanups: Array<() => void> = [];\n\n subgoals.forEach((sub, idx) => {\n if (sub.type === 'click') {\n const onClick = (e: Event): void => {\n const target = e.target;\n if (!(target instanceof Element)) return;\n if (!findClickable(target, node, sub.selector)) return;\n if (goal.type === 'composite') checkComposite(idx);\n else handlers.fireGoal();\n };\n node.addEventListener('click', onClick);\n cleanups.push(() => node.removeEventListener('click', onClick));\n return;\n }\n\n if (sub.type === 'form_submit') {\n const onSubmit = (e: Event): void => {\n if (!(e.target instanceof HTMLFormElement)) return;\n if (!node.contains(e.target)) return;\n if (goal.type === 'composite') checkComposite(idx);\n else handlers.fireGoal();\n };\n node.addEventListener('submit', onSubmit);\n cleanups.push(() => node.removeEventListener('submit', onSubmit));\n return;\n }\n\n if (sub.type === 'scroll_depth') {\n const threshold = Math.max(0, Math.min(1, sub.threshold));\n const io = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.intersectionRatio >= threshold) {\n if (goal.type === 'composite') checkComposite(idx);\n else handlers.fireGoal();\n io.disconnect();\n break;\n }\n }\n },\n { threshold: [threshold] },\n );\n io.observe(node);\n cleanups.push(() => io.disconnect());\n return;\n }\n });\n\n return () => {\n for (const c of cleanups) c();\n };\n}\n\n/**\n * Funnel declaration (spec §7.4): one steps-declaration per funnel per page\n * load; membership is per component and idempotent server-side, so re-sends\n * are harmless but avoided. Only a weighted_composite carries steps — a plain\n * goal declares membership only (the funnel must exist server-side already).\n */\nconst declaredFunnels = new Set<string>();\nconst declaredMemberships = new Set<string>();\n\n/** Test-only: clears the page-load dedup sets. */\nexport function __resetFunnelDeclarations(): void {\n declaredFunnels.clear();\n declaredMemberships.clear();\n}\n\nexport function maybeDeclareFunnel(\n client: SentientClient,\n apiKey: string,\n componentId: string,\n funnelId: string,\n goal: GoalConfig,\n): void {\n const memberKey = `${funnelId}|${componentId}`;\n if (declaredMemberships.has(memberKey)) return;\n declaredMemberships.add(memberKey);\n const withSteps = goal.type === 'weighted_composite' && !declaredFunnels.has(funnelId);\n if (withSteps) declaredFunnels.add(funnelId);\n client.track({\n projectId: apiKey,\n componentId,\n eventType: 'funnel_declared',\n payload: withSteps\n ? {\n funnelId,\n steps: (goal as WeightedCompositeGoal).steps.map((s) => ({ goalId: s.name, weight: s.weight })),\n }\n : { funnelId },\n });\n}\n\n/**\n * Records the `variant_assigned` exposure event.\n */\nexport function trackExposure(\n client: SentientClient,\n apiKey: string,\n componentId: string,\n variantId: string,\n): void {\n client.track({\n projectId: apiKey,\n componentId,\n variantId,\n eventType: 'variant_assigned',\n payload: {},\n });\n}\n","import { isDevBuild } from './adaptive-shared.js';\n\nexport type RegisteredComponent = { id: string; variantIds: string[]; goal?: string };\nexport type RegisteredSlot = {\n id: string;\n arms?: string[];\n dims?: Record<string, readonly string[]>;\n};\n\ntype RegistryState = {\n components: Map<string, RegisteredComponent>;\n slots: Map<string, RegisteredSlot>;\n sections: string[];\n listeners: Set<() => void>;\n /** Bumped on every mutation so `useSyncExternalStore` can read a stable, comparable snapshot. */\n version: number;\n};\n\n// Shared through a window global: the main entry and the /devtools entry are\n// separate bundles, each with its own copy of this module — module-local\n// state would give the devtools an always-empty registry in published apps.\nconst ssrFallback: RegistryState = {\n components: new Map(),\n slots: new Map(),\n sections: [],\n listeners: new Set(),\n version: 0,\n};\n\nfunction state(): RegistryState {\n if (typeof window === 'undefined') return ssrFallback;\n const w = window as unknown as { __sentient_registry?: RegistryState };\n if (!w.__sentient_registry) {\n w.__sentient_registry = {\n components: new Map(),\n slots: new Map(),\n sections: [],\n listeners: new Set(),\n version: 0,\n };\n }\n return w.__sentient_registry;\n}\n\nfunction emit(): void {\n // Bump BEFORE notifying so a useSyncExternalStore consumer re-reading its\n // snapshot inside the notification sees the new value and re-renders.\n state().version += 1;\n for (const fn of state().listeners) fn();\n}\n\n// The registry exists solely to feed the opt-in devtools panel, which is a\n// dev-only surface. In a production build there is no panel reading it, so\n// every register call is dead work plus a `window.__sentient_registry`\n// footprint on the customer's page. Short-circuit to a noop in production —\n// every <Adaptive>/useAdaptive/slot mount calls one of these.\nconst NOOP_UNREGISTER = (): void => undefined;\n\n/** Register (or re-register) a component. Returns an unregister function. */\nexport function registerComponent(c: RegisteredComponent): () => void {\n if (!isDevBuild()) return NOOP_UNREGISTER;\n state().components.set(c.id, c);\n emit();\n return () => {\n state().components.delete(c.id);\n emit();\n };\n}\n\n/** Register (or re-register) a slot declaration. Returns an unregister function. */\nexport function registerSlot(s: RegisteredSlot): () => void {\n if (!isDevBuild()) return NOOP_UNREGISTER;\n state().slots.set(s.id, s);\n emit();\n return () => {\n state().slots.delete(s.id);\n emit();\n };\n}\n\n/** Register the page's declared section ids (from AdaptiveRoot/provider). */\nexport function registerSections(sections: string[]): void {\n if (!isDevBuild()) return;\n state().sections = [...sections];\n emit();\n}\n\nexport function getRegistered(): RegisteredComponent[] {\n return [...state().components.values()];\n}\n\nexport function getRegisteredSlots(): RegisteredSlot[] {\n return [...state().slots.values()];\n}\n\nexport function getRegisteredSections(): string[] {\n return [...state().sections];\n}\n\nexport function subscribeRegistry(fn: () => void): () => void {\n const listeners = state().listeners;\n listeners.add(fn);\n return () => {\n listeners.delete(fn);\n };\n}\n\n/** Monotonic snapshot for `useSyncExternalStore` — changes on every registry mutation. */\nexport function getRegistryVersion(): number {\n return state().version;\n}\n","import { notifyOverridesChanged } from '../override-events.js';\n\nexport type ScenarioWeight = { variantId: string; pulls: number; avgReward: number };\nexport type ScenarioApiOverride =\n | 'error'\n | number\n | { status?: number; body?: unknown; delayMs?: number };\n\nexport type SentientScenario = {\n variants?: Record<string, string>;\n layout?: string[];\n /** Forced persona (canonical PersonaKey). Also sets the persona html attributes. */\n persona?: string;\n /** Persona confidence 0–1; buckets to low/medium/high for the html attribute. Default 1. */\n confidence?: number;\n /** Forced slot results: slot id → arm id (arms slots) or per-dim values (token slots). */\n slots?: Record<string, string | Record<string, string>>;\n weights?: Record<string, ScenarioWeight[]>;\n api?: Record<string, ScenarioApiOverride>;\n};\n\ntype ScenarioWindow = {\n __sentient_overrides?: Record<string, string>;\n __sentient_layout_override?: string[];\n __sentient_slot_overrides?: Record<string, string | Record<string, string>>;\n __sentient_persona_override?: { persona: string; confidence?: number };\n};\n\n/**\n * Confidence → band. Cutoffs pinned to @sentientui/policy `confidenceBand`\n * (<0.3 low, <0.7 medium, else high). Duplicated (not imported) because the\n * Playwright init function is serialized into the page and cannot close\n * over imports — both copies are pinned by tests.\n */\nexport function confidenceBandOf(c: number): 'low' | 'medium' | 'high' {\n return c < 0.3 ? 'low' : c < 0.7 ? 'medium' : 'high';\n}\n\n/** Apply a scenario by setting the client-forcing globals the SDK reads. */\nexport function applyScenario(scenario: SentientScenario = {}): void {\n const w = window as unknown as ScenarioWindow;\n w.__sentient_overrides = { ...(scenario.variants ?? {}) };\n if (scenario.layout) w.__sentient_layout_override = scenario.layout;\n else delete w.__sentient_layout_override;\n if (scenario.slots) w.__sentient_slot_overrides = { ...scenario.slots };\n else delete w.__sentient_slot_overrides;\n if (scenario.persona) {\n w.__sentient_persona_override = {\n persona: scenario.persona,\n confidence: scenario.confidence ?? 1,\n };\n try {\n const d = document.documentElement;\n d.setAttribute('data-sentient-persona', scenario.persona);\n d.setAttribute('data-sentient-confidence', confidenceBandOf(scenario.confidence ?? 1));\n } catch {\n /* no DOM (node env) — the override globals still apply */\n }\n } else {\n delete w.__sentient_persona_override;\n }\n // Ring the override bus so hooks already mounted (useAssignment /\n // useSlotResult / useAdaptivePersona subscribe via useSyncExternalStore)\n // re-render immediately — otherwise forcing state mid-test only takes effect\n // on the next unrelated render.\n notifyOverridesChanged();\n}\n\n/** Clear all forced state. */\nexport function resetScenario(): void {\n const w = window as unknown as ScenarioWindow;\n delete w.__sentient_overrides;\n delete w.__sentient_layout_override;\n delete w.__sentient_slot_overrides;\n delete w.__sentient_persona_override;\n try {\n document.documentElement.removeAttribute('data-sentient-persona');\n document.documentElement.removeAttribute('data-sentient-confidence');\n } catch {\n /* no DOM */\n }\n notifyOverridesChanged();\n}\n","import { http, HttpResponse } from 'msw';\nimport type { RequestHandler } from 'msw';\nimport { resolveScenario } from './resolve.js';\nimport type { SentientScenario } from './scenario.js';\n\n/** Turn a scenario into MSW handlers stubbing every SDK endpoint + capturing events. */\nexport function scenarioToHandlers(scenario: SentientScenario = {}): RequestHandler[] {\n return [\n http.all('*/v1/*', async ({ request }) => {\n const bodyText =\n request.method === 'GET' || request.method === 'HEAD' ? null : await request.text();\n const r = await resolveScenario(scenario, request.method, request.url, bodyText);\n if (!r) return undefined; // not a stubbed route — let MSW handle passthrough\n if (r.json === undefined) return new HttpResponse(null, { status: r.status });\n return HttpResponse.json(r.json as object, { status: r.status });\n }),\n ];\n}\n","export type CapturedEvent = {\n eventType: string;\n goalType?: string;\n componentId?: string;\n variantId?: string;\n [k: string]: unknown;\n};\n\nconst captured: CapturedEvent[] = [];\n\nexport function recordEvent(e: CapturedEvent): void {\n captured.push(e);\n}\n\nexport function getSentientEvents(): CapturedEvent[] {\n return [...captured];\n}\n\nexport function clearSentientEvents(): void {\n captured.length = 0;\n}\n\n/** True if any captured event is a goal (component goal_achieved or named goal) with this name. */\nexport function hasFiredGoal(events: CapturedEvent[], goalName: string): boolean {\n return events.some(\n (e) => (e.eventType === 'goal_achieved' || e.eventType === 'goal') && e.goalType === goalName,\n );\n}\n","import { recordEvent, type CapturedEvent } from './events.js';\nimport { confidenceBandOf, type SentientScenario, type ScenarioApiOverride } from './scenario.js';\n\n/** A framework-agnostic resolved response. `json` undefined ⇒ empty body. */\nexport type ResolvedResponse = { status: number; json?: unknown };\n\nfunction pathOf(url: string): string {\n try { return new URL(url).pathname; } catch { return url.split('?')[0] ?? url; }\n}\n\nasync function apiOverride(scenario: SentientScenario, route: string): Promise<ResolvedResponse | null> {\n const o: ScenarioApiOverride | undefined = scenario.api?.[route];\n if (o === undefined) return null;\n if (o === 'error') return { status: 500 };\n if (typeof o === 'number') return { status: o };\n if (o.delayMs) await new Promise((r) => setTimeout(r, o.delayMs));\n return { status: o.status ?? 200, json: o.body ?? {} };\n}\n\n/**\n * Resolve a request against a scenario. Returns a response, or null for routes\n * outside `/v1/*` (let the caller pass through). Shared by the MSW handlers and\n * the Playwright/Cypress adapters so behaviour can't drift.\n */\nexport async function resolveScenario(\n scenario: SentientScenario,\n _method: string,\n url: string,\n bodyText: string | null,\n): Promise<ResolvedResponse | null> {\n const path = pathOf(url);\n if (!path.includes('/v1/')) return null;\n\n const route = '/v1/' + (path.split('/v1/')[1] ?? '');\n const override = await apiOverride(scenario, route);\n if (override) return override;\n\n const body = bodyText ? (JSON.parse(bodyText) as unknown) : {};\n\n if (route === '/v1/sessions') return { status: 204 };\n\n if (route === '/v1/events') {\n for (const e of body as CapturedEvent[]) recordEvent(e);\n return { status: 204 };\n }\n\n if (route === '/v1/goals') {\n recordEvent({ eventType: 'goal', goalType: (body as { name?: string }).name });\n return { status: 204 };\n }\n\n if (route === '/v1/assign') {\n const b = body as { componentId: string; variantIds?: string[] };\n const variantId = scenario.variants?.[b.componentId] ?? b.variantIds?.[0] ?? 'control';\n return { status: 200, json: { variantId, assignmentTtlMs: 60_000 } };\n }\n\n if (route === '/v1/decide') {\n const b = body as {\n sections?: { id: string }[];\n slots?: Array<{\n id: string;\n arms?: string[];\n dims?: Record<string, string[]>;\n baseline?: string | Record<string, string>;\n }>;\n };\n const layoutOrder = scenario.layout ?? (b.sections ?? []).map((s) => s.id);\n const json: Record<string, unknown> = {\n layoutOrder,\n assignments: scenario.variants ?? {},\n persona: scenario.persona ?? 'unknown',\n confidence: scenario.confidence ?? 1,\n };\n // Mirror the real server: the slots key exists ONLY when slots were requested.\n if (b.slots && b.slots.length > 0) {\n const slots: Record<string, unknown> = {};\n for (const decl of b.slots) {\n slots[decl.id] = scenario.slots?.[decl.id] ?? defaultSlotResult(decl);\n }\n json.slots = slots;\n }\n return { status: 200, json };\n }\n\n if (route === '/v1/explain') {\n const b = body as {\n sections?: { id: string }[];\n persona?: string;\n slots?: Array<{\n id: string;\n arms?: string[];\n dims?: Record<string, string[]>;\n baseline?: string | Record<string, string>;\n }>;\n };\n const layoutOrder = scenario.layout ?? (b.sections ?? []).map((s) => s.id);\n const persona = b.persona ?? scenario.persona ?? 'unknown';\n const confidence = scenario.confidence ?? 1;\n const json: Record<string, unknown> = {\n layoutOrder,\n assignments: scenario.variants ?? {},\n persona,\n reasons: [],\n personaAttributes: { persona, confidence: confidenceBandOf(confidence) },\n };\n if (b.slots && b.slots.length > 0) {\n const slots: Record<string, unknown> = {};\n for (const decl of b.slots) {\n slots[decl.id] = scenario.slots?.[decl.id] ?? defaultSlotResult(decl);\n }\n json.slots = slots;\n }\n return { status: 200, json };\n }\n\n if (route === '/v1/weights') {\n const components = Object.entries(scenario.weights ?? {}).map(([componentId, variants]) => ({ componentId, updatedAt: 0, variants }));\n return { status: 200, json: { components } };\n }\n\n return null;\n}\n\n/** Declared baseline of a slot: explicit `baseline`, else first arm / first value per dim. */\nfunction defaultSlotResult(decl: {\n arms?: string[];\n dims?: Record<string, string[]>;\n baseline?: string | Record<string, string>;\n}): string | Record<string, string> {\n if (decl.arms) {\n return typeof decl.baseline === 'string' ? decl.baseline : decl.arms[0] ?? 'baseline';\n }\n const out: Record<string, string> = {};\n for (const [dim, values] of Object.entries(decl.dims ?? {})) {\n const declared =\n typeof decl.baseline === 'object' && decl.baseline !== null ? decl.baseline[dim] : undefined;\n out[dim] = declared ?? values[0] ?? '';\n }\n return out;\n}\n","import { resolveScenario } from './resolve.js';\nimport { getSentientEvents, type CapturedEvent } from './events.js';\nimport type { SentientScenario } from './scenario.js';\n\ntype InitData = {\n overrides: Record<string, string>;\n layout: string[] | null;\n slots: Record<string, string | Record<string, string>> | null;\n persona: string | null;\n confidence: number;\n};\n\ntype PwRoute = {\n request(): { method(): string; url(): string; postData(): string | null };\n fulfill(r: { status: number; contentType?: string; body?: string }): Promise<void>;\n continue(): Promise<void>;\n};\n\n/** Structural subset of Playwright's `Page` — avoids a hard dependency on @playwright/test. */\ntype PwPage = {\n // Return types are intentionally `Promise<unknown>`: the helper never uses\n // them, and pinning `Promise<void>` breaks against Playwright versions whose\n // `addInitScript`/`route` resolve to `Disposable` rather than `void`.\n addInitScript(script: (arg: InitData) => void, arg: InitData): Promise<unknown>;\n route(url: string, handler: (route: PwRoute) => unknown): Promise<unknown>;\n};\n\n/**\n * Make a Playwright `page` serve a SentientUI scenario: forces\n * variants/layout/slots/persona before load (including the persona html\n * attributes) and stubs every `/v1/*` request from the scenario, capturing\n * posted events. Returns a handle exposing `.events()`.\n */\nexport async function mockSentient(\n page: PwPage,\n scenario: SentientScenario = {},\n): Promise<{ events: () => CapturedEvent[] }> {\n const initData: InitData = {\n overrides: scenario.variants ?? {},\n layout: scenario.layout ?? null,\n slots: scenario.slots ?? null,\n persona: scenario.persona ?? null,\n confidence: scenario.confidence ?? 1,\n };\n await page.addInitScript((data: InitData) => {\n const w = window as unknown as Record<string, unknown>;\n w.__sentient_overrides = data.overrides;\n if (data.layout) w.__sentient_layout_override = data.layout;\n if (data.slots) w.__sentient_slot_overrides = data.slots;\n if (data.persona) {\n w.__sentient_persona_override = { persona: data.persona, confidence: data.confidence };\n try {\n // Inline banding: this function is SERIALIZED into the page context,\n // so it cannot close over imports. Cutoffs pinned to policy\n // confidenceBand (<0.3 low, <0.7 medium, else high).\n const band = data.confidence < 0.3 ? 'low' : data.confidence < 0.7 ? 'medium' : 'high';\n document.documentElement.setAttribute('data-sentient-persona', data.persona);\n document.documentElement.setAttribute('data-sentient-confidence', band);\n } catch {\n /* document not ready — the SDK adopts the override globals instead */\n }\n }\n }, initData);\n\n await page.route('**/v1/**', async (route) => {\n const req = route.request();\n const resolved = await resolveScenario(scenario, req.method(), req.url(), req.postData());\n if (!resolved) return route.continue();\n await route.fulfill({\n status: resolved.status,\n contentType: 'application/json',\n body: JSON.stringify(resolved.json ?? {}),\n });\n });\n\n return { events: () => getSentientEvents() };\n}\n","import { resolveScenario } from './resolve.js';\nimport { confidenceBandOf, type SentientScenario } from './scenario.js';\n\ntype CyReq = {\n method: string;\n url: string;\n body: unknown;\n reply(r: { statusCode: number; body?: unknown }): void;\n};\n\n/** Structural subset of Cypress's `cy` — avoids a hard dependency on cypress. */\ntype Cy = {\n intercept(url: string, handler: (req: CyReq) => void | Promise<void>): unknown;\n on(event: string, cb: (win: Record<string, unknown>) => void): unknown;\n};\n\n/**\n * Make Cypress serve a SentientUI scenario: forces variants/layout/slots/persona\n * on the app window before load (including the persona html attributes) and\n * stubs every `/v1/*` request from the scenario. Call in a `beforeEach` before\n * `cy.visit`.\n *\n * Note: for event assertions in Cypress, alias the intercept (`cy.intercept(...).as('ev')`)\n * and `cy.wait('@ev')` — captured module state does not cross the browser/Node boundary.\n */\nexport function mockSentientCypress(cy: Cy, scenario: SentientScenario = {}): void {\n const overrides = scenario.variants ?? {};\n const layout = scenario.layout ?? null;\n const slots = scenario.slots ?? null;\n const persona = scenario.persona ?? null;\n const confidence = scenario.confidence ?? 1;\n\n cy.on('window:before:load', (win) => {\n win.__sentient_overrides = overrides;\n if (layout) win.__sentient_layout_override = layout;\n if (slots) win.__sentient_slot_overrides = slots;\n if (persona) {\n win.__sentient_persona_override = { persona, confidence };\n try {\n const doc = (win as { document?: Document }).document;\n doc?.documentElement?.setAttribute('data-sentient-persona', persona);\n doc?.documentElement?.setAttribute('data-sentient-confidence', confidenceBandOf(confidence));\n } catch {\n /* document not ready — the SDK adopts the override globals instead */\n }\n }\n });\n\n cy.intercept('**/v1/**', async (req) => {\n const resolved = await resolveScenario(\n scenario,\n req.method,\n req.url,\n req.body != null ? JSON.stringify(req.body) : null,\n );\n if (!resolved) return; // passthrough\n req.reply({ statusCode: resolved.status, body: (resolved.json ?? '') as unknown });\n });\n}\n"],"mappings":"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","names":["render","createContext","useContext","useEffect","useMemo","useRef","useState","useSyncExternalStore","detectDeviceClass","detectTrafficSource","init","store","listeners","update","componentId","weights","store","set","listeners","cb","e","ssrFallback","state","w","getPreviewMode","state","subscribePreview","fn","listeners","createPreviewClient","inner","componentId","segment","variantIds","agentData","agentDataByVariant","slotId","EVENT","notifyOverridesChanged","_a","w","EVENT","publishDevtoolsConfig","config","isDevBuild","_a","ssrFallback","state","w","emit","fn","registerSections","sections","isDevBuild","state","emit","jsx","deriveDefaultSegment","_a","_b","device","detectDeviceClass","source","detectTrafficSource","e","DEFAULT_API_BASE_URL","AdaptiveContext","createContext","useConsentSource","granted","setGranted","useState","sourceRef","useRef","cookie","value","event","hasCheck","useEffect","read","s","want","c","onDecision","AdaptiveProvider","props","client","setClient","sessionSegment","previewOn","setPreviewOn","getPreviewMode","subscribePreview","sourceGranted","consent","prev","config","cancelled","created","stopEngagement","startEngagement","startEngagementCapture","initGraph","__spreadProps","__spreadValues","init","poll","entries","entry","weights","v","update","timerId","ssrFallback","apiBaseUrl","frozenConfigRef","current","key","publishDevtoolsConfig","decided","registerSections","exposedClient","useMemo","createPreviewClient","_c","_d","_e","confidenceBandOf","c","applyScenario","scenario","_a","_b","_c","w","__spreadValues","d","e","notifyOverridesChanged","resetScenario","http","HttpResponse","captured","recordEvent","getSentientEvents","clearSentientEvents","hasFiredGoal","events","goalName","e","pathOf","url","_a","e","apiOverride","scenario","route","_b","_c","o","r","resolveScenario","_method","bodyText","_d","_e","_f","_g","_h","_i","_j","_k","_l","_m","_n","_o","_p","_q","_r","_s","_t","_u","path","override","body","recordEvent","b","json","s","slots","decl","defaultSlotResult","layoutOrder","persona","confidence","confidenceBandOf","componentId","variants","out","dim","values","declared","scenarioToHandlers","scenario","http","request","bodyText","r","resolveScenario","HttpResponse","mockSentient","page","scenario","_a","_b","_c","_d","_e","initData","data","w","band","e","route","req","resolved","resolveScenario","getSentientEvents","mockSentientCypress","cy","scenario","_a","_b","_c","_d","_e","overrides","layout","slots","persona","confidence","win","doc","confidenceBandOf","e","req","resolved","resolveScenario","jsx","renderWithSentient","ui","scenario","options","applyScenario","render","jsx","AdaptiveProvider","setupSentientTests","_a","g","resetScenario"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentientui/react",
3
- "version": "0.22.1",
3
+ "version": "0.22.2",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "sideEffects": false,
@@ -64,8 +64,8 @@
64
64
  "llms.txt"
65
65
  ],
66
66
  "dependencies": {
67
- "@sentientui/core": "0.18.0",
68
- "@sentientui/policy": "0.4.0"
67
+ "@sentientui/core": "0.18.1",
68
+ "@sentientui/policy": "0.5.0"
69
69
  },
70
70
  "peerDependencies": {
71
71
  "@testing-library/react": ">=14.0.0",