@sentientui/core 0.22.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/chunk-2NVSFI4V.mjs +2 -0
  2. package/dist/chunk-2NVSFI4V.mjs.map +1 -0
  3. package/dist/chunk-EWP6FTHE.mjs +2 -0
  4. package/dist/chunk-EWP6FTHE.mjs.map +1 -0
  5. package/dist/chunk-KIP52DUJ.mjs +2 -0
  6. package/dist/chunk-KIP52DUJ.mjs.map +1 -0
  7. package/dist/chunk-RL5B6M4G.mjs +2 -0
  8. package/dist/chunk-RL5B6M4G.mjs.map +1 -0
  9. package/dist/classify-DYlSjkFP.d.cts +47 -0
  10. package/dist/classify-DYlSjkFP.d.ts +47 -0
  11. package/dist/{index-BbrtAtrY.d.ts → index-BjVvN7cU.d.ts} +1 -1
  12. package/dist/{index-CXxvWxCB.d.cts → index-Cd0gMdJv.d.cts} +1 -1
  13. package/dist/index-engagement.d.cts +3 -28
  14. package/dist/index-engagement.d.ts +3 -28
  15. package/dist/index-engagement.js +1 -1
  16. package/dist/index-engagement.js.map +1 -1
  17. package/dist/index-engagement.mjs +1 -1
  18. package/dist/index-engagement.mjs.map +1 -1
  19. package/dist/index-graph.d.cts +14 -4
  20. package/dist/index-graph.d.ts +14 -4
  21. package/dist/index-graph.js +1 -1
  22. package/dist/index-graph.js.map +1 -1
  23. package/dist/index-graph.mjs +1 -1
  24. package/dist/index-graph.mjs.map +1 -1
  25. package/dist/index-local.d.cts +2 -2
  26. package/dist/index-local.d.ts +2 -2
  27. package/dist/index-server.d.cts +10 -2
  28. package/dist/index-server.d.ts +10 -2
  29. package/dist/index-server.js +1 -1
  30. package/dist/index-server.js.map +1 -1
  31. package/dist/index-server.mjs +1 -1
  32. package/dist/index-server.mjs.map +1 -1
  33. package/dist/index-topics.d.cts +21 -0
  34. package/dist/index-topics.d.ts +21 -0
  35. package/dist/index-topics.js +2 -0
  36. package/dist/index-topics.js.map +1 -0
  37. package/dist/index-topics.mjs +2 -0
  38. package/dist/index-topics.mjs.map +1 -0
  39. package/dist/index.d.cts +2 -2
  40. package/dist/index.d.ts +2 -2
  41. package/dist/index.js +1 -1
  42. package/dist/index.js.map +1 -1
  43. package/dist/index.mjs +1 -1
  44. package/dist/{session-meta-BVvq5RBB.d.cts → session-meta-C-bMqVeq.d.cts} +32 -1
  45. package/dist/{session-meta-BVvq5RBB.d.ts → session-meta-C-bMqVeq.d.ts} +32 -1
  46. package/package.json +7 -2
  47. package/dist/chunk-2RH7I6AP.mjs +0 -2
  48. package/dist/chunk-2RH7I6AP.mjs.map +0 -1
  49. package/dist/chunk-CD2A55US.mjs +0 -2
  50. package/dist/chunk-CD2A55US.mjs.map +0 -1
  51. package/dist/chunk-QNEYUJJJ.mjs +0 -2
  52. package/dist/chunk-QNEYUJJJ.mjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index-server.ts","../src/session-meta.ts","../src/storage-key.ts","../src/slots.ts","../src/server.ts"],"sourcesContent":["/**\n * Server-only entry (`@sentientui/core/server`). SSR preload helpers that are\n * never needed in the browser — kept out of the lean client bundle.\n */\n\nexport { preloadAssignments, readSessionCookie, preloadDecisions } from './server.js';\nexport type { ServerAssignConfig, ServerAssignments, DecideResult, SlotDeclInput, SlotResult } from './server.js';\n\nexport {\n deriveSessionSegment,\n buildSessionUpsertPayload,\n detectDeviceClass,\n detectTrafficSource,\n detectTimeOfDay,\n referrerDomainFromReferer,\n} from './session-meta.js';\nexport type { SessionUpsertPayload } from './session-meta.js';\n","/** Session metadata helpers (browser + Node). No DOM APIs. */\n\n/**\n * Known AI-agent / crawler user-agent tokens. Matched case-insensitively as\n * substrings. This is maintained data — bot lists move monthly. Used both to\n * flag automation on sessions (agentic browsers that leak a token) and by the\n * server middleware / agent-feed route to identify crawler HTTP reads.\n */\nexport const agentUaList: readonly string[] = [\n 'GPTBot',\n 'ChatGPT-User',\n 'OAI-SearchBot',\n 'ClaudeBot',\n 'Claude-User',\n 'Claude-SearchBot',\n 'PerplexityBot',\n 'Perplexity-User',\n 'Google-Extended',\n 'Applebot-Extended',\n 'Meta-ExternalAgent',\n 'Bytespider',\n 'CCBot',\n 'Amazonbot',\n 'cohere-ai',\n 'Diffbot',\n];\n\n/** True when the user-agent contains a known AI-agent / crawler token. */\nexport function uaTokenMatch(userAgent: string): boolean {\n return matchedAgentToken(userAgent) !== null;\n}\n\n/** The first known agent token found in the user-agent, or null. */\nexport function matchedAgentToken(userAgent: string): string | null {\n if (!userAgent) return null;\n const s = userAgent.toLowerCase();\n return agentUaList.find((token) => s.includes(token.toLowerCase())) ?? null;\n}\n\n/**\n * Purpose category for an agent fetch, inferred from its published user-agent:\n * - `user` — a person asked an assistant to read the page, live (…-User UAs)\n * - `search` — indexing for an AI answer engine (…-SearchBot / SearchBot)\n * - `training` — model-training crawl (GPTBot, ClaudeBot, CCBot, …)\n * - `other` — not clearly AI, or an unknown/new token\n */\nexport type AgentIntent = 'user' | 'search' | 'training' | 'other';\n\n/** Intent per agent token. Maintained beside `agentUaList` — when you add or\n * rename a token above, set its intent here (a unit test enforces coverage). */\nexport const AGENT_INTENTS: Record<string, AgentIntent> = {\n 'GPTBot': 'training',\n 'ChatGPT-User': 'user',\n 'OAI-SearchBot': 'search',\n 'ClaudeBot': 'training',\n 'Claude-User': 'user',\n 'Claude-SearchBot': 'search',\n 'PerplexityBot': 'search',\n 'Perplexity-User': 'user',\n 'Google-Extended': 'training',\n 'Applebot-Extended': 'training',\n 'Meta-ExternalAgent': 'training',\n 'Bytespider': 'training',\n 'CCBot': 'training',\n 'Amazonbot': 'other',\n 'cohere-ai': 'training',\n 'Diffbot': 'other',\n};\n\n/** Intent for a matched bot token (case-insensitive); `other` for null/unknown. */\nexport function agentIntent(botName: string | null): AgentIntent {\n if (!botName) return 'other';\n const hit = agentUaList.find((t) => t.toLowerCase() === botName.toLowerCase());\n return (hit && AGENT_INTENTS[hit]) || 'other';\n}\n\n/** `agentUaList` grouped by intent — the \"which crawlers do you classify?\" reference. */\nexport function classifiedAgents(): Record<AgentIntent, string[]> {\n const out: Record<AgentIntent, string[]> = { user: [], search: [], training: [], other: [] };\n for (const t of agentUaList) out[AGENT_INTENTS[t] ?? 'other'].push(t);\n return out;\n}\n\nexport function detectDeviceClass(userAgent: string): string {\n const s = userAgent.toLowerCase();\n if (/ipad|tablet|playbook|kindle|silk/.test(s)) return 'tablet';\n if (/mobi|iphone|ipod|android.*mobile|phone/.test(s)) return 'mobile';\n return 'desktop';\n}\n\nexport function detectTrafficSource(referrer: string, appOrigin?: string): string {\n if (!referrer) return 'direct';\n try {\n const refUrl = new URL(referrer);\n if (appOrigin) {\n try {\n if (new URL(appOrigin).host === refUrl.host) return 'direct';\n } catch {\n /* ignore invalid appOrigin */\n }\n }\n const host = refUrl.hostname.toLowerCase();\n if (/(^|\\.)(google|bing|duckduckgo|yahoo)\\./.test(host)) return 'search';\n // Anchor to the registrable domain (exact host or a subdomain of it) so\n // hosts like `x.company.com`, `t.company.io` or `linkedinsights.com` are not\n // misclassified as social by an unbounded substring match.\n if (/(^|\\.)(twitter\\.com|x\\.com|facebook\\.com|linkedin\\.com|reddit\\.com|t\\.co)$/.test(host)) return 'social';\n return 'referral';\n } catch {\n return 'direct';\n }\n}\n\nexport function referrerDomainFromReferer(referrer: string): string | null {\n if (!referrer) return null;\n try {\n return new URL(referrer).hostname;\n } catch {\n return null;\n }\n}\n\nexport function detectTimeOfDay(d: Date): string {\n const h = d.getHours();\n if (h < 6) return 'night';\n if (h < 12) return 'morning';\n if (h < 18) return 'afternoon';\n return 'evening';\n}\n\nexport type SessionUpsertPayload = {\n sessionId: string;\n ephemeral: boolean;\n utmParams: Record<string, string>;\n deviceClass: string;\n trafficSource: string;\n referrerDomain: string | null;\n timeOfDay: string;\n dayOfWeek: string;\n /**\n * True when this session is likely driven by automation — either\n * `navigator.webdriver` was set, or the user-agent carried a known agent\n * token. Probabilistic: a flag for metrics + bandit exclusion, never a gate.\n */\n automation: boolean;\n};\n\n/** Bandit segment key: `<device_class>:<traffic_source>`. */\nexport function deriveSessionSegment(opts?: {\n userAgent?: string;\n referer?: string;\n appOrigin?: string;\n}): string {\n const body = buildSessionUpsertPayload('__segment__', opts);\n return `${body.deviceClass}:${body.trafficSource}`;\n}\n\n/**\n * Builds a session upsert body aligned with the browser SDK so SSR assign uses\n * the same segment key (`device:source`) as the client after hydration.\n */\nexport function buildSessionUpsertPayload(\n sessionId: string,\n opts?: {\n userAgent?: string;\n referer?: string;\n appOrigin?: string;\n utmParams?: Record<string, string>;\n now?: Date;\n /** `navigator.webdriver` value from the browser, when available. */\n webdriver?: boolean;\n },\n): SessionUpsertPayload {\n const ua = opts?.userAgent?.trim() ?? '';\n const referer = opts?.referer?.trim() ?? '';\n const now = opts?.now ?? new Date();\n return {\n sessionId,\n ephemeral: false,\n utmParams: opts?.utmParams ?? {},\n deviceClass: ua ? detectDeviceClass(ua) : 'desktop',\n trafficSource: referer\n ? detectTrafficSource(referer, opts?.appOrigin)\n : 'direct',\n referrerDomain: referrerDomainFromReferer(referer),\n timeOfDay: detectTimeOfDay(now),\n dayOfWeek: ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'][now.getDay()] ?? 'sun',\n automation: opts?.webdriver === true || uaTokenMatch(ua),\n };\n}\n","/**\n * Per-project browser-storage namespace suffix, derived from the public apiKey.\n *\n * Multiple SentientUI projects (different `pk_` keys) can run on the same exact\n * origin. Storage keyed only by name (`_snt_uid`, `_snt_asgn_*`,\n * `_snt_graph_nodes`) would then collide across projects — the visitor id in\n * particular, whose session row is keyed globally server-side, would let one\n * project's traffic land on another's session. Suffixing every browser key with\n * the apiKey prefix isolates projects, matching the queue's existing\n * `_snt_retry_${apiKey.slice(0,12)}` convention.\n *\n * Returns `''` when no apiKey is available (local mode) so keys stay stable\n * there.\n */\nexport function storageSuffix(apiKey?: string): string {\n return apiKey ? `_${apiKey.slice(0, 12)}` : '';\n}\n\n/**\n * The bare, pre-namespacing session cookie name. Still read as a FALLBACK\n * everywhere the suffixed name is read, so visitors who got their identity\n * before per-project namespacing keep it instead of being minted a fresh one.\n */\nexport const LEGACY_SESSION_COOKIE_NAME = '_snt_uid';\n\n/**\n * The one place the session cookie's name is decided. The writer (session.ts)\n * and every reader (core/server.ts SSR helper, graph sync, react devtools) must\n * call THIS — when namespacing landed, the writer moved to the suffixed name\n * while three readers kept the bare `_snt_uid`, so every SSR request for a\n * returning visitor missed the cookie and minted a fresh orphan session (quota\n * inflation, broken sticky assignments and persona continuity), and graph sync\n * sent `sessionId: undefined`. Deriving both sides from one function makes that\n * drift impossible, and index.test.ts pins the reader against the writer.\n */\nexport function sessionCookieName(apiKey?: string): string {\n return `${LEGACY_SESSION_COOKIE_NAME}${storageSuffix(apiKey)}`;\n}\n","/**\n * Slot declaration helpers shared by the browser client (decide) and the\n * server preload path. Pure wrappers over @sentientui/policy.\n */\nimport {\n canonicalArm,\n slotBaselineArm,\n slotResultFor,\n type SlotDecl,\n type SlotResult,\n} from '@sentientui/policy';\n\n/** SDK-facing slot declaration. `dims` accepts readonly arrays (`as const`). */\nexport type SlotDeclInput = {\n id: string;\n arms?: string[];\n dims?: Record<string, readonly string[]>;\n baseline?: string | Record<string, string>;\n};\n\nexport type { SlotResult };\n\n/**\n * Whitelists the wire fields of a slot declaration. Anything an SDK layer\n * attached (goal configs, refs, …) is stripped so it never reaches the zod\n * schema on the API. Also normalizes readonly arrays to mutable ones.\n */\nexport function toWireSlot(d: SlotDeclInput): SlotDecl {\n return {\n id: d.id,\n ...(d.arms ? { arms: [...d.arms] } : {}),\n ...(d.dims\n ? {\n dims: Object.fromEntries(\n Object.entries(d.dims).map(([dim, values]) => [dim, [...values]]),\n ),\n }\n : {}),\n ...(d.baseline !== undefined ? { baseline: d.baseline } : {}),\n };\n}\n\n/** The declared (or default first-declared) baseline result for a slot. */\nexport function baselineResultFor(d: SlotDeclInput): SlotResult {\n const decl = toWireSlot(d);\n return slotResultFor(decl, slotBaselineArm(decl));\n}\n\n/** Baseline results for a whole declaration list, keyed by slot id. */\nexport function baselineSlots(decls: SlotDeclInput[]): Record<string, SlotResult> {\n const out: Record<string, SlotResult> = {};\n for (const d of decls) out[d.id] = baselineResultFor(d);\n return out;\n}\n\n/**\n * Canonical arm string of a slot result: dims results encode as sorted\n * `dim=value` pairs joined with '|'; arms results are the arm id verbatim.\n */\nexport function armOfResult(result: SlotResult): string {\n return typeof result === 'string' ? result : canonicalArm(result);\n}\n","/**\n * Server-side helpers for SSR variant pre-loading.\n * Pure fetch — no DOM APIs. Safe in Node.js, Edge, and Deno runtimes.\n */\nimport { buildSessionUpsertPayload } from './session-meta.js';\nimport { LEGACY_SESSION_COOKIE_NAME, sessionCookieName } from './storage-key.js';\nimport {\n toWireSlot,\n baselineResultFor,\n baselineSlots,\n type SlotDeclInput,\n type SlotResult,\n} from './slots.js';\n\nexport type { SlotDeclInput, SlotResult };\n\nexport type ServerAssignConfig = {\n /** Public API key (pk_...). */\n apiKey: string;\n /**\n * Base URL of the Sentient API without trailing slash.\n * e.g. 'https://api.yourapp.com/v1'\n */\n baseUrl: string;\n /**\n * Browser origin of your app (e.g. `http://localhost:3001`). Required for `pk_`\n * keys — server-side fetch must send the same `Origin` the API allows.\n */\n origin?: string;\n /** From `User-Agent` request header (Next.js `headers()`). */\n userAgent?: string;\n /** From `Referer` request header. */\n referer?: string;\n utmParams?: Record<string, string>;\n /**\n * Set true when the request carries a tracking opt-out — `DNT: 1` or\n * `Sec-GPC: 1`. When set, the SSR helpers skip the session upsert and the\n * assign/decide call entirely: the page renders defaults/baseline and no\n * session row is minted for the visitor (audit P4). The client SDK already\n * no-ops for these visitors; this keeps the server from minting a session +\n * assignment on every page view before the client can.\n */\n doNotTrack?: boolean;\n /**\n * Declared persona — the role your app already knows for this visitor\n * (e.g. from your auth context during SSR). Same contract as the client\n * option: must be a vocabulary key; unrecognized values are ignored\n * server-side. Never a user id or email.\n */\n persona?: string;\n /**\n * Milliseconds to wait for the API before returning default variants.\n * Prevents slow/cold API from blocking SSR. Defaults to 1000 — the hot path\n * is served from in-process caches and typically returns in well under\n * 150 ms. The full 1 s budget is only reached on a cold start or an API\n * geographically distant from your SSR host, after which defaults render\n * with no layout shift. Lower it if your API is co-located and warm.\n */\n timeoutMs?: number;\n};\n\n/** componentId → assigned variantId */\nexport type ServerAssignments = Record<string, string>;\n\ntype AssignResult = { variantId: string; assignmentTtlMs: number };\n\nconst DEFAULT_TIMEOUT_MS = 1000;\n\nfunction fetchWithTimeout(\n url: string,\n init: RequestInit,\n timeoutMs: number,\n): Promise<Response> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n return fetch(url, { ...init, signal: controller.signal }).finally(() =>\n clearTimeout(timer),\n );\n}\n\n/**\n * Fetches variant assignments server-side for all listed components.\n * Returns a map suitable for passing as `initialAssignments` to `<AdaptiveProvider>`.\n *\n * Call from Next.js layout, Server Component, or getServerSideProps.\n *\n * @example\n * ```tsx\n * const assignments = await preloadAssignments(\n * [\n * { id: 'hero', variantIds: ['hero-a', 'hero-b'] },\n * { id: 'cta', variantIds: ['cta-short', 'cta-long'] },\n * ],\n * sessionId, // read from cookies() or req.cookies\n * { apiKey: process.env.NEXT_PUBLIC_SENTIENT_API_KEY!, baseUrl: 'https://api.sentient-ui.com/v1' },\n * );\n * return <AdaptiveProvider ... initialAssignments={assignments}>{children}</AdaptiveProvider>;\n * ```\n */\nexport async function preloadAssignments(\n components: Array<{ id: string; variantIds: string[] }>,\n sessionId: string,\n config: ServerAssignConfig,\n): Promise<ServerAssignments> {\n // Opted-out visitor (DNT/GPC): render defaults, mint no session (audit P4).\n if (config.doNotTrack) return {};\n\n const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${config.apiKey}`,\n };\n if (config.origin) {\n headers.Origin = config.origin;\n }\n\n // Session metadata must match the browser SDK so assign seeds variant_weights\n // under the same segment (not `unknown:unknown`).\n const sessionBody = {\n ...buildSessionUpsertPayload(sessionId, {\n userAgent: config.userAgent,\n referer: config.referer,\n utmParams: config.utmParams,\n appOrigin: config.origin,\n }),\n ...(config.persona ? { persona: config.persona } : {}),\n };\n\n try {\n const res = await fetchWithTimeout(\n `${config.baseUrl}/sessions`,\n { method: 'POST', headers, body: JSON.stringify(sessionBody) },\n timeoutMs,\n );\n if (res.status === 402) {\n console.warn(\n '[SentientUI] Session limit exceeded for this project. The bandit will stop learning until the limit resets. Upgrade at sentient-ui.com/pricing',\n );\n } else if (!res.ok) {\n const body = await res.json().catch(() => ({}));\n console.error(`[SentientUI] preloadAssignments: session upsert failed (${res.status})`, body);\n }\n } catch (err) {\n console.error('[SentientUI] preloadAssignments: session upsert threw', err);\n }\n\n const results = await Promise.allSettled(\n components.map(async ({ id, variantIds }) => {\n const res = await fetchWithTimeout(\n `${config.baseUrl}/assign`,\n { method: 'POST', headers, body: JSON.stringify({ sessionId, componentId: id, variantIds }) },\n timeoutMs,\n );\n if (!res.ok) {\n const body = await res.json().catch(() => ({}));\n console.error(`[SentientUI] preloadAssignments: assign failed for \"${id}\" (${res.status})`, body);\n return null;\n }\n const body = (await res.json()) as AssignResult;\n return { id, variantId: body.variantId };\n }),\n );\n\n const assignments: ServerAssignments = {};\n for (const result of results) {\n if (result.status === 'fulfilled' && result.value) {\n assignments[result.value.id] = result.value.variantId;\n }\n }\n return assignments;\n}\n\n/**\n * Reads the Sentient session cookie from a Next.js `ReadonlyRequestCookies` object\n * (the return value of `cookies()` from `next/headers`), or any object with a\n * `get(name: string)` method. Returns null if the cookie is absent.\n *\n * Pass the project's `apiKey`: the client writes the per-project SUFFIXED name\n * (`sessionCookieName` — see storage-key.ts), so reading only the bare\n * `_snt_uid` missed the cookie on every SSR request for a returning visitor and\n * minted a fresh orphan session each page view (quota inflation, broken sticky\n * assignments and persona continuity). The bare name is still read as a\n * fallback for identities minted before namespacing.\n */\nexport function readSessionCookie(\n cookies: { get(name: string): { value: string } | undefined },\n apiKey?: string,\n): string | null {\n return (\n (apiKey ? cookies.get(sessionCookieName(apiKey))?.value : undefined) ??\n cookies.get(LEGACY_SESSION_COOKIE_NAME)?.value ??\n null\n );\n}\n\nexport type DecideResult = {\n layoutOrder: string[];\n assignments: Record<string, string>;\n /** Slot results keyed by slot id. Baseline-resolved when the API omits/fails them. */\n slots: Record<string, SlotResult>;\n persona: string;\n confidence: number;\n};\n\n/**\n * Single-roundtrip SSR call returning layout order, component assignments,\n * and adaptive-slot results. Falls back to default section order + empty\n * assignments + baseline slots if the API is unavailable. A response without\n * a `slots` field means the server predates slots — every declared slot\n * resolves to its baseline (no retry).\n */\nexport async function preloadDecisions(\n params: {\n sections?: string[];\n components: Array<{ id: string; variantIds?: string[] }>;\n slots?: SlotDeclInput[];\n },\n sessionId: string,\n config: ServerAssignConfig,\n): Promise<DecideResult> {\n const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const declaredSlots = params.slots ?? [];\n const fallback: DecideResult = {\n layoutOrder: params.sections ?? [],\n assignments: {},\n slots: baselineSlots(declaredSlots),\n persona: 'unknown',\n confidence: 0,\n };\n\n // Opted-out visitor (DNT/GPC): baseline layout/slots, mint no session (audit P4).\n if (config.doNotTrack) return fallback;\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${config.apiKey}`,\n };\n if (config.origin) headers.Origin = config.origin;\n\n const sessionBody = {\n ...buildSessionUpsertPayload(sessionId, {\n userAgent: config.userAgent,\n referer: config.referer,\n utmParams: config.utmParams,\n appOrigin: config.origin,\n }),\n ...(config.persona ? { persona: config.persona } : {}),\n };\n\n try {\n const sessionRes = await fetchWithTimeout(\n `${config.baseUrl}/sessions`,\n { method: 'POST', headers, body: JSON.stringify(sessionBody) },\n timeoutMs,\n );\n if (!sessionRes.ok) {\n const body = await sessionRes.json().catch(() => ({}));\n console.error(`[SentientUI] preloadDecisions: session upsert failed (${sessionRes.status})`, body);\n }\n } catch (err) {\n console.error('[SentientUI] preloadDecisions: session upsert threw', err);\n }\n\n try {\n const res = await fetchWithTimeout(\n `${config.baseUrl}/decide`,\n {\n method: 'POST',\n headers,\n body: JSON.stringify({\n sessionId,\n sections: (params.sections ?? []).map((id) => ({ id })),\n components: params.components,\n ...(declaredSlots.length > 0 ? { slots: declaredSlots.map(toWireSlot) } : {}),\n ...(config.persona ? { persona: config.persona } : {}),\n }),\n },\n timeoutMs,\n );\n if (!res.ok) {\n const body = await res.json().catch(() => ({}));\n console.error(`[SentientUI] preloadDecisions: decide failed (${res.status})`, body);\n return fallback;\n }\n const data = (await res.json()) as {\n layoutOrder?: string[];\n assignments?: Record<string, string>;\n slots?: Record<string, SlotResult>;\n persona?: string;\n confidence?: number;\n };\n\n const slots: Record<string, SlotResult> = {};\n for (const d of declaredSlots) {\n // `data.slots === undefined` ⇒ server predates slots: baseline, no retry.\n slots[d.id] = data.slots?.[d.id] ?? baselineResultFor(d);\n }\n\n return {\n layoutOrder: data.layoutOrder ?? params.sections ?? [],\n assignments: data.assignments ?? {},\n slots,\n persona: data.persona ?? 'unknown',\n confidence: data.confidence ?? 0,\n };\n } catch (err) {\n console.error('[SentientUI] preloadDecisions: decide threw', err);\n return fallback;\n }\n}\n"],"mappings":"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","names":["index_server_exports","__export","buildSessionUpsertPayload","deriveSessionSegment","detectDeviceClass","detectTimeOfDay","detectTrafficSource","preloadAssignments","preloadDecisions","readSessionCookie","referrerDomainFromReferer","__toCommonJS","agentUaList","uaTokenMatch","userAgent","matchedAgentToken","_a","s","token","detectDeviceClass","userAgent","s","detectTrafficSource","referrer","appOrigin","refUrl","e","host","referrerDomainFromReferer","detectTimeOfDay","d","h","deriveSessionSegment","opts","body","buildSessionUpsertPayload","sessionId","_a","_b","_c","_d","_e","_f","_g","ua","referer","now","uaTokenMatch","storageSuffix","apiKey","LEGACY_SESSION_COOKIE_NAME","sessionCookieName","import_policy","toWireSlot","d","__spreadValues","dim","values","baselineResultFor","decl","baselineSlots","decls","out","DEFAULT_TIMEOUT_MS","fetchWithTimeout","url","init","timeoutMs","controller","timer","__spreadProps","__spreadValues","preloadAssignments","components","sessionId","config","_a","headers","sessionBody","buildSessionUpsertPayload","res","body","err","results","id","variantIds","assignments","result","readSessionCookie","cookies","apiKey","_b","_c","_d","sessionCookieName","LEGACY_SESSION_COOKIE_NAME","preloadDecisions","params","_e","_f","_g","_h","_i","_j","_k","declaredSlots","fallback","baselineSlots","sessionRes","toWireSlot","data","slots","d","baselineResultFor"]}
1
+ {"version":3,"sources":["../src/index-server.ts","../src/session-meta.ts","../src/storage-key.ts","../src/slots.ts","../src/server.ts"],"sourcesContent":["/**\n * Server-only entry (`@sentientui/core/server`). SSR preload helpers that are\n * never needed in the browser — kept out of the lean client bundle.\n */\n\nexport { preloadAssignments, readSessionCookie, preloadDecisions } from './server.js';\nexport type { ServerAssignConfig, ServerAssignments, DecideResult, SlotDeclInput, SlotResult } from './server.js';\n\nexport {\n deriveSessionSegment,\n buildSessionUpsertPayload,\n detectDeviceClass,\n detectTrafficSource,\n detectTimeOfDay,\n extractTrackedParams,\n CLICK_ID_KEYS,\n referrerDomainFromReferer,\n} from './session-meta.js';\nexport type { SessionUpsertPayload } from './session-meta.js';\n","/** Session metadata helpers (browser + Node). No DOM APIs. */\n\n/**\n * Known AI-agent / crawler user-agent tokens. Matched case-insensitively as\n * substrings. This is maintained data — bot lists move monthly. Used both to\n * flag automation on sessions (agentic browsers that leak a token) and by the\n * server middleware / agent-feed route to identify crawler HTTP reads.\n */\nexport const agentUaList: readonly string[] = [\n 'GPTBot',\n 'ChatGPT-User',\n 'OAI-SearchBot',\n 'ClaudeBot',\n 'Claude-User',\n 'Claude-SearchBot',\n 'PerplexityBot',\n 'Perplexity-User',\n 'Google-Extended',\n 'Applebot-Extended',\n 'Meta-ExternalAgent',\n 'Bytespider',\n 'CCBot',\n 'Amazonbot',\n 'cohere-ai',\n 'Diffbot',\n];\n\n/** True when the user-agent contains a known AI-agent / crawler token. */\nexport function uaTokenMatch(userAgent: string): boolean {\n return matchedAgentToken(userAgent) !== null;\n}\n\n/** The first known agent token found in the user-agent, or null. */\nexport function matchedAgentToken(userAgent: string): string | null {\n if (!userAgent) return null;\n const s = userAgent.toLowerCase();\n return agentUaList.find((token) => s.includes(token.toLowerCase())) ?? null;\n}\n\n/**\n * Purpose category for an agent fetch, inferred from its published user-agent:\n * - `user` — a person asked an assistant to read the page, live (…-User UAs)\n * - `search` — indexing for an AI answer engine (…-SearchBot / SearchBot)\n * - `training` — model-training crawl (GPTBot, ClaudeBot, CCBot, …)\n * - `other` — not clearly AI, or an unknown/new token\n */\nexport type AgentIntent = 'user' | 'search' | 'training' | 'other';\n\n/** Intent per agent token. Maintained beside `agentUaList` — when you add or\n * rename a token above, set its intent here (a unit test enforces coverage). */\nexport const AGENT_INTENTS: Record<string, AgentIntent> = {\n 'GPTBot': 'training',\n 'ChatGPT-User': 'user',\n 'OAI-SearchBot': 'search',\n 'ClaudeBot': 'training',\n 'Claude-User': 'user',\n 'Claude-SearchBot': 'search',\n 'PerplexityBot': 'search',\n 'Perplexity-User': 'user',\n 'Google-Extended': 'training',\n 'Applebot-Extended': 'training',\n 'Meta-ExternalAgent': 'training',\n 'Bytespider': 'training',\n 'CCBot': 'training',\n 'Amazonbot': 'other',\n 'cohere-ai': 'training',\n 'Diffbot': 'other',\n};\n\n/** Intent for a matched bot token (case-insensitive); `other` for null/unknown. */\nexport function agentIntent(botName: string | null): AgentIntent {\n if (!botName) return 'other';\n const hit = agentUaList.find((t) => t.toLowerCase() === botName.toLowerCase());\n return (hit && AGENT_INTENTS[hit]) || 'other';\n}\n\n/** `agentUaList` grouped by intent — the \"which crawlers do you classify?\" reference. */\nexport function classifiedAgents(): Record<AgentIntent, string[]> {\n const out: Record<AgentIntent, string[]> = { user: [], search: [], training: [], other: [] };\n for (const t of agentUaList) out[AGENT_INTENTS[t] ?? 'other'].push(t);\n return out;\n}\n\nexport function detectDeviceClass(userAgent: string): string {\n const s = userAgent.toLowerCase();\n if (/ipad|tablet|playbook|kindle|silk/.test(s)) return 'tablet';\n if (/mobi|iphone|ipod|android.*mobile|phone/.test(s)) return 'mobile';\n return 'desktop';\n}\n\nexport function detectTrafficSource(referrer: string, appOrigin?: string): string {\n if (!referrer) return 'direct';\n try {\n const refUrl = new URL(referrer);\n if (appOrigin) {\n try {\n if (new URL(appOrigin).host === refUrl.host) return 'direct';\n } catch {\n /* ignore invalid appOrigin */\n }\n }\n const host = refUrl.hostname.toLowerCase();\n if (/(^|\\.)(google|bing|duckduckgo|yahoo)\\./.test(host)) return 'search';\n // Anchor to the registrable domain (exact host or a subdomain of it) so\n // hosts like `x.company.com`, `t.company.io` or `linkedinsights.com` are not\n // misclassified as social by an unbounded substring match.\n if (/(^|\\.)(twitter\\.com|x\\.com|facebook\\.com|linkedin\\.com|reddit\\.com|t\\.co)$/.test(host)) return 'social';\n return 'referral';\n } catch {\n return 'direct';\n }\n}\n\nexport function referrerDomainFromReferer(referrer: string): string | null {\n if (!referrer) return null;\n try {\n return new URL(referrer).hostname;\n } catch {\n return null;\n }\n}\n\n/**\n * Ad-platform click-ID query params worth keeping. An ALLOWLIST, unlike the\n * `utm_` prefix match: unknown query keys here are unbounded-cardinality junk\n * (session tokens, cache busters) that would bloat the sessions table, so only\n * the IDs the major ad platforms actually append survive.\n *\n * gclid / gbraid / wbraid — Google Ads auto-tagging (search, YouTube, Display;\n * gbraid/wbraid are the iOS-14 privacy variants)\n * fbclid — Meta (Facebook + Instagram). Appended to EVERY outbound Meta\n * click, paid and organic alike — it identifies the platform, never spend.\n * ttclid — TikTok Ads\n * msclkid — Microsoft Ads (Bing)\n * twclid — X/Twitter Ads\n * li_fat_id — LinkedIn Ads\n */\nexport const CLICK_ID_KEYS: readonly string[] = [\n 'gclid',\n 'gbraid',\n 'wbraid',\n 'fbclid',\n 'ttclid',\n 'msclkid',\n 'twclid',\n 'li_fat_id',\n];\n\n/**\n * Splits a URL query into the attribution params the session upsert carries:\n * every `utm_`-prefixed key, plus allowlisted ad click IDs (CLICK_ID_KEYS).\n * Accepts a raw search string (\"?a=b\" or \"a=b\"), a URLSearchParams, or a\n * Next.js `searchParams` object (whose values may be string arrays — the\n * first occurrence wins, matching URLSearchParams iteration order).\n * Node-safe: no DOM APIs.\n */\nexport function extractTrackedParams(\n search: string | URLSearchParams | Record<string, string | string[] | undefined>,\n): { utmParams: Record<string, string>; clickIds: Record<string, string> } {\n const utmParams: Record<string, string> = {};\n const clickIds: Record<string, string> = {};\n try {\n const entries: Iterable<[string, string]> =\n typeof search === 'string' || search instanceof URLSearchParams\n ? new URLSearchParams(search)\n : Object.entries(search).flatMap(([k, v]): Array<[string, string]> => {\n const first = Array.isArray(v) ? v[0] : v;\n return first === undefined ? [] : [[k, first]];\n });\n for (const [k, v] of entries) {\n // First occurrence wins for duplicates — a repeated gclid in a mangled\n // URL must not let the later value silently replace the real one.\n if (k.startsWith('utm_')) {\n if (!(k in utmParams)) utmParams[k] = v;\n } else if (CLICK_ID_KEYS.includes(k)) {\n if (!(k in clickIds)) clickIds[k] = v;\n }\n }\n } catch {\n /* malformed input → empty attribution, never a throw at init() */\n }\n return { utmParams, clickIds };\n}\n\nexport function detectTimeOfDay(d: Date): string {\n const h = d.getHours();\n if (h < 6) return 'night';\n if (h < 12) return 'morning';\n if (h < 18) return 'afternoon';\n return 'evening';\n}\n\nexport type SessionUpsertPayload = {\n sessionId: string;\n ephemeral: boolean;\n utmParams: Record<string, string>;\n /** Allowlisted ad-platform click IDs from the landing URL (CLICK_ID_KEYS). */\n clickIds: Record<string, string>;\n deviceClass: string;\n trafficSource: string;\n referrerDomain: string | null;\n timeOfDay: string;\n dayOfWeek: string;\n /**\n * True when this session is likely driven by automation — either\n * `navigator.webdriver` was set, or the user-agent carried a known agent\n * token. Probabilistic: a flag for metrics + bandit exclusion, never a gate.\n */\n automation: boolean;\n};\n\n/** Bandit segment key: `<device_class>:<traffic_source>`. */\nexport function deriveSessionSegment(opts?: {\n userAgent?: string;\n referer?: string;\n appOrigin?: string;\n}): string {\n const body = buildSessionUpsertPayload('__segment__', opts);\n return `${body.deviceClass}:${body.trafficSource}`;\n}\n\n/**\n * Builds a session upsert body aligned with the browser SDK so SSR assign uses\n * the same segment key (`device:source`) as the client after hydration.\n */\nexport function buildSessionUpsertPayload(\n sessionId: string,\n opts?: {\n userAgent?: string;\n referer?: string;\n appOrigin?: string;\n utmParams?: Record<string, string>;\n clickIds?: Record<string, string>;\n now?: Date;\n /** `navigator.webdriver` value from the browser, when available. */\n webdriver?: boolean;\n },\n): SessionUpsertPayload {\n const ua = opts?.userAgent?.trim() ?? '';\n const referer = opts?.referer?.trim() ?? '';\n const now = opts?.now ?? new Date();\n return {\n sessionId,\n ephemeral: false,\n utmParams: opts?.utmParams ?? {},\n clickIds: opts?.clickIds ?? {},\n deviceClass: ua ? detectDeviceClass(ua) : 'desktop',\n trafficSource: referer\n ? detectTrafficSource(referer, opts?.appOrigin)\n : 'direct',\n referrerDomain: referrerDomainFromReferer(referer),\n timeOfDay: detectTimeOfDay(now),\n dayOfWeek: ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'][now.getDay()] ?? 'sun',\n automation: opts?.webdriver === true || uaTokenMatch(ua),\n };\n}\n","/**\n * Per-project browser-storage namespace suffix, derived from the public apiKey.\n *\n * Multiple SentientUI projects (different `pk_` keys) can run on the same exact\n * origin. Storage keyed only by name (`_snt_uid`, `_snt_asgn_*`,\n * `_snt_graph_nodes`) would then collide across projects — the visitor id in\n * particular, whose session row is keyed globally server-side, would let one\n * project's traffic land on another's session. Suffixing every browser key with\n * the apiKey prefix isolates projects, matching the queue's existing\n * `_snt_retry_${apiKey.slice(0,12)}` convention.\n *\n * Returns `''` when no apiKey is available (local mode) so keys stay stable\n * there.\n */\nexport function storageSuffix(apiKey?: string): string {\n return apiKey ? `_${apiKey.slice(0, 12)}` : '';\n}\n\n/**\n * The bare, pre-namespacing session cookie name. Still read as a FALLBACK\n * everywhere the suffixed name is read, so visitors who got their identity\n * before per-project namespacing keep it instead of being minted a fresh one.\n */\nexport const LEGACY_SESSION_COOKIE_NAME = '_snt_uid';\n\n/**\n * The one place the session cookie's name is decided. The writer (session.ts)\n * and every reader (core/server.ts SSR helper, graph sync, react devtools) must\n * call THIS — when namespacing landed, the writer moved to the suffixed name\n * while three readers kept the bare `_snt_uid`, so every SSR request for a\n * returning visitor missed the cookie and minted a fresh orphan session (quota\n * inflation, broken sticky assignments and persona continuity), and graph sync\n * sent `sessionId: undefined`. Deriving both sides from one function makes that\n * drift impossible, and index.test.ts pins the reader against the writer.\n */\nexport function sessionCookieName(apiKey?: string): string {\n return `${LEGACY_SESSION_COOKIE_NAME}${storageSuffix(apiKey)}`;\n}\n","/**\n * Slot declaration helpers shared by the browser client (decide) and the\n * server preload path. Pure wrappers over @sentientui/policy.\n */\nimport {\n canonicalArm,\n slotBaselineArm,\n slotResultFor,\n type SlotDecl,\n type SlotResult,\n} from '@sentientui/policy';\n\n/** SDK-facing slot declaration. `dims` accepts readonly arrays (`as const`). */\nexport type SlotDeclInput = {\n id: string;\n arms?: string[];\n dims?: Record<string, readonly string[]>;\n baseline?: string | Record<string, string>;\n};\n\nexport type { SlotResult };\n\n/**\n * Whitelists the wire fields of a slot declaration. Anything an SDK layer\n * attached (goal configs, refs, …) is stripped so it never reaches the zod\n * schema on the API. Also normalizes readonly arrays to mutable ones.\n */\nexport function toWireSlot(d: SlotDeclInput): SlotDecl {\n return {\n id: d.id,\n ...(d.arms ? { arms: [...d.arms] } : {}),\n ...(d.dims\n ? {\n dims: Object.fromEntries(\n Object.entries(d.dims).map(([dim, values]) => [dim, [...values]]),\n ),\n }\n : {}),\n ...(d.baseline !== undefined ? { baseline: d.baseline } : {}),\n };\n}\n\n/** The declared (or default first-declared) baseline result for a slot. */\nexport function baselineResultFor(d: SlotDeclInput): SlotResult {\n const decl = toWireSlot(d);\n return slotResultFor(decl, slotBaselineArm(decl));\n}\n\n/** Baseline results for a whole declaration list, keyed by slot id. */\nexport function baselineSlots(decls: SlotDeclInput[]): Record<string, SlotResult> {\n const out: Record<string, SlotResult> = {};\n for (const d of decls) out[d.id] = baselineResultFor(d);\n return out;\n}\n\n/**\n * Canonical arm string of a slot result: dims results encode as sorted\n * `dim=value` pairs joined with '|'; arms results are the arm id verbatim.\n */\nexport function armOfResult(result: SlotResult): string {\n return typeof result === 'string' ? result : canonicalArm(result);\n}\n","/**\n * Server-side helpers for SSR variant pre-loading.\n * Pure fetch — no DOM APIs. Safe in Node.js, Edge, and Deno runtimes.\n */\nimport { buildSessionUpsertPayload } from './session-meta.js';\nimport { LEGACY_SESSION_COOKIE_NAME, sessionCookieName } from './storage-key.js';\nimport {\n toWireSlot,\n baselineResultFor,\n baselineSlots,\n type SlotDeclInput,\n type SlotResult,\n} from './slots.js';\n\nexport type { SlotDeclInput, SlotResult };\n\nexport type ServerAssignConfig = {\n /** Public API key (pk_...). */\n apiKey: string;\n /**\n * Base URL of the Sentient API without trailing slash.\n * e.g. 'https://api.yourapp.com/v1'\n */\n baseUrl: string;\n /**\n * Browser origin of your app (e.g. `http://localhost:3001`). Required for `pk_`\n * keys — server-side fetch must send the same `Origin` the API allows.\n */\n origin?: string;\n /** From `User-Agent` request header (Next.js `headers()`). */\n userAgent?: string;\n /** From `Referer` request header. */\n referer?: string;\n utmParams?: Record<string, string>;\n /**\n * Ad-platform click IDs (gclid, fbclid, ttclid, …) from the landing URL.\n * Derive both this and `utmParams` with `extractTrackedParams(searchParams)`\n * — the Referer header never carries the landing page's own query string, so\n * without these two fields SSR-minted sessions have no campaign attribution\n * until the client SDK hydrates.\n */\n clickIds?: Record<string, string>;\n /**\n * Set true when the request carries a tracking opt-out — `DNT: 1` or\n * `Sec-GPC: 1`. When set, the SSR helpers skip the session upsert and the\n * assign/decide call entirely: the page renders defaults/baseline and no\n * session row is minted for the visitor (audit P4). The client SDK already\n * no-ops for these visitors; this keeps the server from minting a session +\n * assignment on every page view before the client can.\n */\n doNotTrack?: boolean;\n /**\n * Declared persona — the role your app already knows for this visitor\n * (e.g. from your auth context during SSR). Same contract as the client\n * option: must be a vocabulary key; unrecognized values are ignored\n * server-side. Never a user id or email.\n */\n persona?: string;\n /**\n * Milliseconds to wait for the API before returning default variants.\n * Prevents slow/cold API from blocking SSR. Defaults to 1000 — the hot path\n * is served from in-process caches and typically returns in well under\n * 150 ms. The full 1 s budget is only reached on a cold start or an API\n * geographically distant from your SSR host, after which defaults render\n * with no layout shift. Lower it if your API is co-located and warm.\n */\n timeoutMs?: number;\n};\n\n/** componentId → assigned variantId */\nexport type ServerAssignments = Record<string, string>;\n\ntype AssignResult = { variantId: string; assignmentTtlMs: number };\n\nconst DEFAULT_TIMEOUT_MS = 1000;\n\nfunction fetchWithTimeout(\n url: string,\n init: RequestInit,\n timeoutMs: number,\n): Promise<Response> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n return fetch(url, { ...init, signal: controller.signal }).finally(() =>\n clearTimeout(timer),\n );\n}\n\n/**\n * Fetches variant assignments server-side for all listed components.\n * Returns a map suitable for passing as `initialAssignments` to `<AdaptiveProvider>`.\n *\n * Call from Next.js layout, Server Component, or getServerSideProps.\n *\n * @example\n * ```tsx\n * const assignments = await preloadAssignments(\n * [\n * { id: 'hero', variantIds: ['hero-a', 'hero-b'] },\n * { id: 'cta', variantIds: ['cta-short', 'cta-long'] },\n * ],\n * sessionId, // read from cookies() or req.cookies\n * { apiKey: process.env.NEXT_PUBLIC_SENTIENT_API_KEY!, baseUrl: 'https://api.sentient-ui.com/v1' },\n * );\n * return <AdaptiveProvider ... initialAssignments={assignments}>{children}</AdaptiveProvider>;\n * ```\n */\nexport async function preloadAssignments(\n components: Array<{ id: string; variantIds: string[] }>,\n sessionId: string,\n config: ServerAssignConfig,\n): Promise<ServerAssignments> {\n // Opted-out visitor (DNT/GPC): render defaults, mint no session (audit P4).\n if (config.doNotTrack) return {};\n\n const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${config.apiKey}`,\n };\n if (config.origin) {\n headers.Origin = config.origin;\n }\n\n // Session metadata must match the browser SDK so assign seeds variant_weights\n // under the same segment (not `unknown:unknown`).\n const sessionBody = {\n ...buildSessionUpsertPayload(sessionId, {\n userAgent: config.userAgent,\n referer: config.referer,\n utmParams: config.utmParams,\n clickIds: config.clickIds,\n appOrigin: config.origin,\n }),\n ...(config.persona ? { persona: config.persona } : {}),\n };\n\n try {\n const res = await fetchWithTimeout(\n `${config.baseUrl}/sessions`,\n { method: 'POST', headers, body: JSON.stringify(sessionBody) },\n timeoutMs,\n );\n if (res.status === 402) {\n console.warn(\n '[SentientUI] Session limit exceeded for this project. The bandit will stop learning until the limit resets. Upgrade at sentient-ui.com/pricing',\n );\n } else if (!res.ok) {\n const body = await res.json().catch(() => ({}));\n console.error(`[SentientUI] preloadAssignments: session upsert failed (${res.status})`, body);\n }\n } catch (err) {\n console.error('[SentientUI] preloadAssignments: session upsert threw', err);\n }\n\n const results = await Promise.allSettled(\n components.map(async ({ id, variantIds }) => {\n const res = await fetchWithTimeout(\n `${config.baseUrl}/assign`,\n { method: 'POST', headers, body: JSON.stringify({ sessionId, componentId: id, variantIds }) },\n timeoutMs,\n );\n if (!res.ok) {\n const body = await res.json().catch(() => ({}));\n console.error(`[SentientUI] preloadAssignments: assign failed for \"${id}\" (${res.status})`, body);\n return null;\n }\n const body = (await res.json()) as AssignResult;\n return { id, variantId: body.variantId };\n }),\n );\n\n const assignments: ServerAssignments = {};\n for (const result of results) {\n if (result.status === 'fulfilled' && result.value) {\n assignments[result.value.id] = result.value.variantId;\n }\n }\n return assignments;\n}\n\n/**\n * Reads the Sentient session cookie from a Next.js `ReadonlyRequestCookies` object\n * (the return value of `cookies()` from `next/headers`), or any object with a\n * `get(name: string)` method. Returns null if the cookie is absent.\n *\n * Pass the project's `apiKey`: the client writes the per-project SUFFIXED name\n * (`sessionCookieName` — see storage-key.ts), so reading only the bare\n * `_snt_uid` missed the cookie on every SSR request for a returning visitor and\n * minted a fresh orphan session each page view (quota inflation, broken sticky\n * assignments and persona continuity). The bare name is still read as a\n * fallback for identities minted before namespacing.\n */\nexport function readSessionCookie(\n cookies: { get(name: string): { value: string } | undefined },\n apiKey?: string,\n): string | null {\n return (\n (apiKey ? cookies.get(sessionCookieName(apiKey))?.value : undefined) ??\n cookies.get(LEGACY_SESSION_COOKIE_NAME)?.value ??\n null\n );\n}\n\nexport type DecideResult = {\n layoutOrder: string[];\n assignments: Record<string, string>;\n /** Slot results keyed by slot id. Baseline-resolved when the API omits/fails them. */\n slots: Record<string, SlotResult>;\n persona: string;\n confidence: number;\n};\n\n/**\n * Single-roundtrip SSR call returning layout order, component assignments,\n * and adaptive-slot results. Falls back to default section order + empty\n * assignments + baseline slots if the API is unavailable. A response without\n * a `slots` field means the server predates slots — every declared slot\n * resolves to its baseline (no retry).\n */\nexport async function preloadDecisions(\n params: {\n sections?: string[];\n components: Array<{ id: string; variantIds?: string[] }>;\n slots?: SlotDeclInput[];\n },\n sessionId: string,\n config: ServerAssignConfig,\n): Promise<DecideResult> {\n const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const declaredSlots = params.slots ?? [];\n const fallback: DecideResult = {\n layoutOrder: params.sections ?? [],\n assignments: {},\n slots: baselineSlots(declaredSlots),\n persona: 'unknown',\n confidence: 0,\n };\n\n // Opted-out visitor (DNT/GPC): baseline layout/slots, mint no session (audit P4).\n if (config.doNotTrack) return fallback;\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${config.apiKey}`,\n };\n if (config.origin) headers.Origin = config.origin;\n\n const sessionBody = {\n ...buildSessionUpsertPayload(sessionId, {\n userAgent: config.userAgent,\n referer: config.referer,\n utmParams: config.utmParams,\n clickIds: config.clickIds,\n appOrigin: config.origin,\n }),\n ...(config.persona ? { persona: config.persona } : {}),\n };\n\n try {\n const sessionRes = await fetchWithTimeout(\n `${config.baseUrl}/sessions`,\n { method: 'POST', headers, body: JSON.stringify(sessionBody) },\n timeoutMs,\n );\n if (!sessionRes.ok) {\n const body = await sessionRes.json().catch(() => ({}));\n console.error(`[SentientUI] preloadDecisions: session upsert failed (${sessionRes.status})`, body);\n }\n } catch (err) {\n console.error('[SentientUI] preloadDecisions: session upsert threw', err);\n }\n\n try {\n const res = await fetchWithTimeout(\n `${config.baseUrl}/decide`,\n {\n method: 'POST',\n headers,\n body: JSON.stringify({\n sessionId,\n sections: (params.sections ?? []).map((id) => ({ id })),\n components: params.components,\n ...(declaredSlots.length > 0 ? { slots: declaredSlots.map(toWireSlot) } : {}),\n ...(config.persona ? { persona: config.persona } : {}),\n }),\n },\n timeoutMs,\n );\n if (!res.ok) {\n const body = await res.json().catch(() => ({}));\n console.error(`[SentientUI] preloadDecisions: decide failed (${res.status})`, body);\n return fallback;\n }\n const data = (await res.json()) as {\n layoutOrder?: string[];\n assignments?: Record<string, string>;\n slots?: Record<string, SlotResult>;\n persona?: string;\n confidence?: number;\n };\n\n const slots: Record<string, SlotResult> = {};\n for (const d of declaredSlots) {\n // `data.slots === undefined` ⇒ server predates slots: baseline, no retry.\n slots[d.id] = data.slots?.[d.id] ?? baselineResultFor(d);\n }\n\n return {\n layoutOrder: data.layoutOrder ?? params.sections ?? [],\n assignments: data.assignments ?? {},\n slots,\n persona: data.persona ?? 'unknown',\n confidence: data.confidence ?? 0,\n };\n } catch (err) {\n console.error('[SentientUI] preloadDecisions: decide threw', err);\n return fallback;\n }\n}\n"],"mappings":"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","names":["index_server_exports","__export","CLICK_ID_KEYS","buildSessionUpsertPayload","deriveSessionSegment","detectDeviceClass","detectTimeOfDay","detectTrafficSource","extractTrackedParams","preloadAssignments","preloadDecisions","readSessionCookie","referrerDomainFromReferer","__toCommonJS","agentUaList","uaTokenMatch","userAgent","matchedAgentToken","_a","s","token","detectDeviceClass","userAgent","s","detectTrafficSource","referrer","appOrigin","refUrl","e","host","referrerDomainFromReferer","CLICK_ID_KEYS","extractTrackedParams","search","utmParams","clickIds","entries","k","v","first","detectTimeOfDay","d","h","deriveSessionSegment","opts","body","buildSessionUpsertPayload","sessionId","_a","_b","_c","_d","_e","_f","_g","_h","ua","referer","now","uaTokenMatch","storageSuffix","apiKey","LEGACY_SESSION_COOKIE_NAME","sessionCookieName","import_policy","toWireSlot","d","__spreadValues","dim","values","baselineResultFor","decl","baselineSlots","decls","out","DEFAULT_TIMEOUT_MS","fetchWithTimeout","url","init","timeoutMs","controller","timer","__spreadProps","__spreadValues","preloadAssignments","components","sessionId","config","_a","headers","sessionBody","buildSessionUpsertPayload","res","body","err","results","id","variantIds","assignments","result","readSessionCookie","cookies","apiKey","_b","_c","_d","sessionCookieName","LEGACY_SESSION_COOKIE_NAME","preloadDecisions","params","_e","_f","_g","_h","_i","_j","_k","declaredSlots","fallback","baselineSlots","sessionRes","toWireSlot","data","slots","d","baselineResultFor"]}
@@ -1,2 +1,2 @@
1
- import{b as U,c as D,j,k as $,l as N,m as M,n as E,o as b,p as P,q as C,r as k}from"./chunk-QNEYUJJJ.mjs";import{a as l,b as T}from"./chunk-TMCGHANO.mjs";var x=1e3;function A(r,i,e){let o=new AbortController,t=setTimeout(()=>o.abort(),e);return fetch(r,T(l({},i),{signal:o.signal})).finally(()=>clearTimeout(t))}async function _(r,i,e){var g;if(e.doNotTrack)return{};let o=(g=e.timeoutMs)!=null?g:x,t={"Content-Type":"application/json",Authorization:`Bearer ${e.apiKey}`};e.origin&&(t.Origin=e.origin);let a=l(l({},b(i,{userAgent:e.userAgent,referer:e.referer,utmParams:e.utmParams,appOrigin:e.origin})),e.persona?{persona:e.persona}:{});try{let s=await A(`${e.baseUrl}/sessions`,{method:"POST",headers:t,body:JSON.stringify(a)},o);if(s.status===402)console.warn("[SentientUI] Session limit exceeded for this project. The bandit will stop learning until the limit resets. Upgrade at sentient-ui.com/pricing");else if(!s.ok){let p=await s.json().catch(()=>({}));console.error(`[SentientUI] preloadAssignments: session upsert failed (${s.status})`,p)}}catch(s){console.error("[SentientUI] preloadAssignments: session upsert threw",s)}let m=await Promise.allSettled(r.map(async({id:s,variantIds:p})=>{let d=await A(`${e.baseUrl}/assign`,{method:"POST",headers:t,body:JSON.stringify({sessionId:i,componentId:s,variantIds:p})},o);if(!d.ok){let f=await d.json().catch(()=>({}));return console.error(`[SentientUI] preloadAssignments: assign failed for "${s}" (${d.status})`,f),null}let y=await d.json();return{id:s,variantId:y.variantId}})),S={};for(let s of m)s.status==="fulfilled"&&s.value&&(S[s.value.id]=s.value.variantId);return S}function B(r,i){var e,o,t,a;return(a=(t=i?(e=r.get(D(i)))==null?void 0:e.value:void 0)!=null?t:(o=r.get(U))==null?void 0:o.value)!=null?a:null}async function J(r,i,e){var g,s,p,d,y,f,R,h,v,O,I;let o=(g=e.timeoutMs)!=null?g:x,t=(s=r.slots)!=null?s:[],a={layoutOrder:(p=r.sections)!=null?p:[],assignments:{},slots:k(t),persona:"unknown",confidence:0};if(e.doNotTrack)return a;let m={"Content-Type":"application/json",Authorization:`Bearer ${e.apiKey}`};e.origin&&(m.Origin=e.origin);let S=l(l({},b(i,{userAgent:e.userAgent,referer:e.referer,utmParams:e.utmParams,appOrigin:e.origin})),e.persona?{persona:e.persona}:{});try{let n=await A(`${e.baseUrl}/sessions`,{method:"POST",headers:m,body:JSON.stringify(S)},o);if(!n.ok){let c=await n.json().catch(()=>({}));console.error(`[SentientUI] preloadDecisions: session upsert failed (${n.status})`,c)}}catch(n){console.error("[SentientUI] preloadDecisions: session upsert threw",n)}try{let n=await A(`${e.baseUrl}/decide`,{method:"POST",headers:m,body:JSON.stringify(l(l({sessionId:i,sections:((d=r.sections)!=null?d:[]).map(u=>({id:u})),components:r.components},t.length>0?{slots:t.map(P)}:{}),e.persona?{persona:e.persona}:{}))},o);if(!n.ok){let u=await n.json().catch(()=>({}));return console.error(`[SentientUI] preloadDecisions: decide failed (${n.status})`,u),a}let c=await n.json(),w={};for(let u of t)w[u.id]=(f=(y=c.slots)==null?void 0:y[u.id])!=null?f:C(u);return{layoutOrder:(h=(R=c.layoutOrder)!=null?R:r.sections)!=null?h:[],assignments:(v=c.assignments)!=null?v:{},slots:w,persona:(O=c.persona)!=null?O:"unknown",confidence:(I=c.confidence)!=null?I:0}}catch(n){return console.error("[SentientUI] preloadDecisions: decide threw",n),a}}export{b as buildSessionUpsertPayload,E as deriveSessionSegment,j as detectDeviceClass,M as detectTimeOfDay,$ as detectTrafficSource,_ as preloadAssignments,J as preloadDecisions,B as readSessionCookie,N as referrerDomainFromReferer};
1
+ import{b as D,c as U,j,k as $,l as N,m as M,n as E,o as _,p as K,q as b,r as k,s as P,t as C}from"./chunk-2NVSFI4V.mjs";import{a as l,b as w}from"./chunk-TMCGHANO.mjs";var x=1e3;function A(r,i,e){let o=new AbortController,t=setTimeout(()=>o.abort(),e);return fetch(r,w(l({},i),{signal:o.signal})).finally(()=>clearTimeout(t))}async function B(r,i,e){var g;if(e.doNotTrack)return{};let o=(g=e.timeoutMs)!=null?g:x,t={"Content-Type":"application/json",Authorization:`Bearer ${e.apiKey}`};e.origin&&(t.Origin=e.origin);let a=l(l({},b(i,{userAgent:e.userAgent,referer:e.referer,utmParams:e.utmParams,clickIds:e.clickIds,appOrigin:e.origin})),e.persona?{persona:e.persona}:{});try{let s=await A(`${e.baseUrl}/sessions`,{method:"POST",headers:t,body:JSON.stringify(a)},o);if(s.status===402)console.warn("[SentientUI] Session limit exceeded for this project. The bandit will stop learning until the limit resets. Upgrade at sentient-ui.com/pricing");else if(!s.ok){let p=await s.json().catch(()=>({}));console.error(`[SentientUI] preloadAssignments: session upsert failed (${s.status})`,p)}}catch(s){console.error("[SentientUI] preloadAssignments: session upsert threw",s)}let m=await Promise.allSettled(r.map(async({id:s,variantIds:p})=>{let c=await A(`${e.baseUrl}/assign`,{method:"POST",headers:t,body:JSON.stringify({sessionId:i,componentId:s,variantIds:p})},o);if(!c.ok){let f=await c.json().catch(()=>({}));return console.error(`[SentientUI] preloadAssignments: assign failed for "${s}" (${c.status})`,f),null}let y=await c.json();return{id:s,variantId:y.variantId}})),S={};for(let s of m)s.status==="fulfilled"&&s.value&&(S[s.value.id]=s.value.variantId);return S}function J(r,i){var e,o,t,a;return(a=(t=i?(e=r.get(U(i)))==null?void 0:e.value:void 0)!=null?t:(o=r.get(D))==null?void 0:o.value)!=null?a:null}async function F(r,i,e){var g,s,p,c,y,f,I,R,h,v,O;let o=(g=e.timeoutMs)!=null?g:x,t=(s=r.slots)!=null?s:[],a={layoutOrder:(p=r.sections)!=null?p:[],assignments:{},slots:C(t),persona:"unknown",confidence:0};if(e.doNotTrack)return a;let m={"Content-Type":"application/json",Authorization:`Bearer ${e.apiKey}`};e.origin&&(m.Origin=e.origin);let S=l(l({},b(i,{userAgent:e.userAgent,referer:e.referer,utmParams:e.utmParams,clickIds:e.clickIds,appOrigin:e.origin})),e.persona?{persona:e.persona}:{});try{let n=await A(`${e.baseUrl}/sessions`,{method:"POST",headers:m,body:JSON.stringify(S)},o);if(!n.ok){let d=await n.json().catch(()=>({}));console.error(`[SentientUI] preloadDecisions: session upsert failed (${n.status})`,d)}}catch(n){console.error("[SentientUI] preloadDecisions: session upsert threw",n)}try{let n=await A(`${e.baseUrl}/decide`,{method:"POST",headers:m,body:JSON.stringify(l(l({sessionId:i,sections:((c=r.sections)!=null?c:[]).map(u=>({id:u})),components:r.components},t.length>0?{slots:t.map(k)}:{}),e.persona?{persona:e.persona}:{}))},o);if(!n.ok){let u=await n.json().catch(()=>({}));return console.error(`[SentientUI] preloadDecisions: decide failed (${n.status})`,u),a}let d=await n.json(),T={};for(let u of t)T[u.id]=(f=(y=d.slots)==null?void 0:y[u.id])!=null?f:P(u);return{layoutOrder:(R=(I=d.layoutOrder)!=null?I:r.sections)!=null?R:[],assignments:(h=d.assignments)!=null?h:{},slots:T,persona:(v=d.persona)!=null?v:"unknown",confidence:(O=d.confidence)!=null?O:0}}catch(n){return console.error("[SentientUI] preloadDecisions: decide threw",n),a}}export{M as CLICK_ID_KEYS,b as buildSessionUpsertPayload,K as deriveSessionSegment,j as detectDeviceClass,_ as detectTimeOfDay,$ as detectTrafficSource,E as extractTrackedParams,B as preloadAssignments,F as preloadDecisions,J as readSessionCookie,N as referrerDomainFromReferer};
2
2
  //# sourceMappingURL=index-server.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server.ts"],"sourcesContent":["/**\n * Server-side helpers for SSR variant pre-loading.\n * Pure fetch — no DOM APIs. Safe in Node.js, Edge, and Deno runtimes.\n */\nimport { buildSessionUpsertPayload } from './session-meta.js';\nimport { LEGACY_SESSION_COOKIE_NAME, sessionCookieName } from './storage-key.js';\nimport {\n toWireSlot,\n baselineResultFor,\n baselineSlots,\n type SlotDeclInput,\n type SlotResult,\n} from './slots.js';\n\nexport type { SlotDeclInput, SlotResult };\n\nexport type ServerAssignConfig = {\n /** Public API key (pk_...). */\n apiKey: string;\n /**\n * Base URL of the Sentient API without trailing slash.\n * e.g. 'https://api.yourapp.com/v1'\n */\n baseUrl: string;\n /**\n * Browser origin of your app (e.g. `http://localhost:3001`). Required for `pk_`\n * keys — server-side fetch must send the same `Origin` the API allows.\n */\n origin?: string;\n /** From `User-Agent` request header (Next.js `headers()`). */\n userAgent?: string;\n /** From `Referer` request header. */\n referer?: string;\n utmParams?: Record<string, string>;\n /**\n * Set true when the request carries a tracking opt-out — `DNT: 1` or\n * `Sec-GPC: 1`. When set, the SSR helpers skip the session upsert and the\n * assign/decide call entirely: the page renders defaults/baseline and no\n * session row is minted for the visitor (audit P4). The client SDK already\n * no-ops for these visitors; this keeps the server from minting a session +\n * assignment on every page view before the client can.\n */\n doNotTrack?: boolean;\n /**\n * Declared persona — the role your app already knows for this visitor\n * (e.g. from your auth context during SSR). Same contract as the client\n * option: must be a vocabulary key; unrecognized values are ignored\n * server-side. Never a user id or email.\n */\n persona?: string;\n /**\n * Milliseconds to wait for the API before returning default variants.\n * Prevents slow/cold API from blocking SSR. Defaults to 1000 — the hot path\n * is served from in-process caches and typically returns in well under\n * 150 ms. The full 1 s budget is only reached on a cold start or an API\n * geographically distant from your SSR host, after which defaults render\n * with no layout shift. Lower it if your API is co-located and warm.\n */\n timeoutMs?: number;\n};\n\n/** componentId → assigned variantId */\nexport type ServerAssignments = Record<string, string>;\n\ntype AssignResult = { variantId: string; assignmentTtlMs: number };\n\nconst DEFAULT_TIMEOUT_MS = 1000;\n\nfunction fetchWithTimeout(\n url: string,\n init: RequestInit,\n timeoutMs: number,\n): Promise<Response> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n return fetch(url, { ...init, signal: controller.signal }).finally(() =>\n clearTimeout(timer),\n );\n}\n\n/**\n * Fetches variant assignments server-side for all listed components.\n * Returns a map suitable for passing as `initialAssignments` to `<AdaptiveProvider>`.\n *\n * Call from Next.js layout, Server Component, or getServerSideProps.\n *\n * @example\n * ```tsx\n * const assignments = await preloadAssignments(\n * [\n * { id: 'hero', variantIds: ['hero-a', 'hero-b'] },\n * { id: 'cta', variantIds: ['cta-short', 'cta-long'] },\n * ],\n * sessionId, // read from cookies() or req.cookies\n * { apiKey: process.env.NEXT_PUBLIC_SENTIENT_API_KEY!, baseUrl: 'https://api.sentient-ui.com/v1' },\n * );\n * return <AdaptiveProvider ... initialAssignments={assignments}>{children}</AdaptiveProvider>;\n * ```\n */\nexport async function preloadAssignments(\n components: Array<{ id: string; variantIds: string[] }>,\n sessionId: string,\n config: ServerAssignConfig,\n): Promise<ServerAssignments> {\n // Opted-out visitor (DNT/GPC): render defaults, mint no session (audit P4).\n if (config.doNotTrack) return {};\n\n const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${config.apiKey}`,\n };\n if (config.origin) {\n headers.Origin = config.origin;\n }\n\n // Session metadata must match the browser SDK so assign seeds variant_weights\n // under the same segment (not `unknown:unknown`).\n const sessionBody = {\n ...buildSessionUpsertPayload(sessionId, {\n userAgent: config.userAgent,\n referer: config.referer,\n utmParams: config.utmParams,\n appOrigin: config.origin,\n }),\n ...(config.persona ? { persona: config.persona } : {}),\n };\n\n try {\n const res = await fetchWithTimeout(\n `${config.baseUrl}/sessions`,\n { method: 'POST', headers, body: JSON.stringify(sessionBody) },\n timeoutMs,\n );\n if (res.status === 402) {\n console.warn(\n '[SentientUI] Session limit exceeded for this project. The bandit will stop learning until the limit resets. Upgrade at sentient-ui.com/pricing',\n );\n } else if (!res.ok) {\n const body = await res.json().catch(() => ({}));\n console.error(`[SentientUI] preloadAssignments: session upsert failed (${res.status})`, body);\n }\n } catch (err) {\n console.error('[SentientUI] preloadAssignments: session upsert threw', err);\n }\n\n const results = await Promise.allSettled(\n components.map(async ({ id, variantIds }) => {\n const res = await fetchWithTimeout(\n `${config.baseUrl}/assign`,\n { method: 'POST', headers, body: JSON.stringify({ sessionId, componentId: id, variantIds }) },\n timeoutMs,\n );\n if (!res.ok) {\n const body = await res.json().catch(() => ({}));\n console.error(`[SentientUI] preloadAssignments: assign failed for \"${id}\" (${res.status})`, body);\n return null;\n }\n const body = (await res.json()) as AssignResult;\n return { id, variantId: body.variantId };\n }),\n );\n\n const assignments: ServerAssignments = {};\n for (const result of results) {\n if (result.status === 'fulfilled' && result.value) {\n assignments[result.value.id] = result.value.variantId;\n }\n }\n return assignments;\n}\n\n/**\n * Reads the Sentient session cookie from a Next.js `ReadonlyRequestCookies` object\n * (the return value of `cookies()` from `next/headers`), or any object with a\n * `get(name: string)` method. Returns null if the cookie is absent.\n *\n * Pass the project's `apiKey`: the client writes the per-project SUFFIXED name\n * (`sessionCookieName` — see storage-key.ts), so reading only the bare\n * `_snt_uid` missed the cookie on every SSR request for a returning visitor and\n * minted a fresh orphan session each page view (quota inflation, broken sticky\n * assignments and persona continuity). The bare name is still read as a\n * fallback for identities minted before namespacing.\n */\nexport function readSessionCookie(\n cookies: { get(name: string): { value: string } | undefined },\n apiKey?: string,\n): string | null {\n return (\n (apiKey ? cookies.get(sessionCookieName(apiKey))?.value : undefined) ??\n cookies.get(LEGACY_SESSION_COOKIE_NAME)?.value ??\n null\n );\n}\n\nexport type DecideResult = {\n layoutOrder: string[];\n assignments: Record<string, string>;\n /** Slot results keyed by slot id. Baseline-resolved when the API omits/fails them. */\n slots: Record<string, SlotResult>;\n persona: string;\n confidence: number;\n};\n\n/**\n * Single-roundtrip SSR call returning layout order, component assignments,\n * and adaptive-slot results. Falls back to default section order + empty\n * assignments + baseline slots if the API is unavailable. A response without\n * a `slots` field means the server predates slots — every declared slot\n * resolves to its baseline (no retry).\n */\nexport async function preloadDecisions(\n params: {\n sections?: string[];\n components: Array<{ id: string; variantIds?: string[] }>;\n slots?: SlotDeclInput[];\n },\n sessionId: string,\n config: ServerAssignConfig,\n): Promise<DecideResult> {\n const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const declaredSlots = params.slots ?? [];\n const fallback: DecideResult = {\n layoutOrder: params.sections ?? [],\n assignments: {},\n slots: baselineSlots(declaredSlots),\n persona: 'unknown',\n confidence: 0,\n };\n\n // Opted-out visitor (DNT/GPC): baseline layout/slots, mint no session (audit P4).\n if (config.doNotTrack) return fallback;\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${config.apiKey}`,\n };\n if (config.origin) headers.Origin = config.origin;\n\n const sessionBody = {\n ...buildSessionUpsertPayload(sessionId, {\n userAgent: config.userAgent,\n referer: config.referer,\n utmParams: config.utmParams,\n appOrigin: config.origin,\n }),\n ...(config.persona ? { persona: config.persona } : {}),\n };\n\n try {\n const sessionRes = await fetchWithTimeout(\n `${config.baseUrl}/sessions`,\n { method: 'POST', headers, body: JSON.stringify(sessionBody) },\n timeoutMs,\n );\n if (!sessionRes.ok) {\n const body = await sessionRes.json().catch(() => ({}));\n console.error(`[SentientUI] preloadDecisions: session upsert failed (${sessionRes.status})`, body);\n }\n } catch (err) {\n console.error('[SentientUI] preloadDecisions: session upsert threw', err);\n }\n\n try {\n const res = await fetchWithTimeout(\n `${config.baseUrl}/decide`,\n {\n method: 'POST',\n headers,\n body: JSON.stringify({\n sessionId,\n sections: (params.sections ?? []).map((id) => ({ id })),\n components: params.components,\n ...(declaredSlots.length > 0 ? { slots: declaredSlots.map(toWireSlot) } : {}),\n ...(config.persona ? { persona: config.persona } : {}),\n }),\n },\n timeoutMs,\n );\n if (!res.ok) {\n const body = await res.json().catch(() => ({}));\n console.error(`[SentientUI] preloadDecisions: decide failed (${res.status})`, body);\n return fallback;\n }\n const data = (await res.json()) as {\n layoutOrder?: string[];\n assignments?: Record<string, string>;\n slots?: Record<string, SlotResult>;\n persona?: string;\n confidence?: number;\n };\n\n const slots: Record<string, SlotResult> = {};\n for (const d of declaredSlots) {\n // `data.slots === undefined` ⇒ server predates slots: baseline, no retry.\n slots[d.id] = data.slots?.[d.id] ?? baselineResultFor(d);\n }\n\n return {\n layoutOrder: data.layoutOrder ?? params.sections ?? [],\n assignments: data.assignments ?? {},\n slots,\n persona: data.persona ?? 'unknown',\n confidence: data.confidence ?? 0,\n };\n } catch (err) {\n console.error('[SentientUI] preloadDecisions: decide threw', err);\n return fallback;\n }\n}\n"],"mappings":"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","names":["DEFAULT_TIMEOUT_MS","fetchWithTimeout","url","init","timeoutMs","controller","timer","__spreadProps","__spreadValues","preloadAssignments","components","sessionId","config","_a","headers","sessionBody","buildSessionUpsertPayload","res","body","err","results","id","variantIds","assignments","result","readSessionCookie","cookies","apiKey","_b","_c","_d","sessionCookieName","LEGACY_SESSION_COOKIE_NAME","preloadDecisions","params","_e","_f","_g","_h","_i","_j","_k","declaredSlots","fallback","baselineSlots","sessionRes","toWireSlot","data","slots","d","baselineResultFor"]}
1
+ {"version":3,"sources":["../src/server.ts"],"sourcesContent":["/**\n * Server-side helpers for SSR variant pre-loading.\n * Pure fetch — no DOM APIs. Safe in Node.js, Edge, and Deno runtimes.\n */\nimport { buildSessionUpsertPayload } from './session-meta.js';\nimport { LEGACY_SESSION_COOKIE_NAME, sessionCookieName } from './storage-key.js';\nimport {\n toWireSlot,\n baselineResultFor,\n baselineSlots,\n type SlotDeclInput,\n type SlotResult,\n} from './slots.js';\n\nexport type { SlotDeclInput, SlotResult };\n\nexport type ServerAssignConfig = {\n /** Public API key (pk_...). */\n apiKey: string;\n /**\n * Base URL of the Sentient API without trailing slash.\n * e.g. 'https://api.yourapp.com/v1'\n */\n baseUrl: string;\n /**\n * Browser origin of your app (e.g. `http://localhost:3001`). Required for `pk_`\n * keys — server-side fetch must send the same `Origin` the API allows.\n */\n origin?: string;\n /** From `User-Agent` request header (Next.js `headers()`). */\n userAgent?: string;\n /** From `Referer` request header. */\n referer?: string;\n utmParams?: Record<string, string>;\n /**\n * Ad-platform click IDs (gclid, fbclid, ttclid, …) from the landing URL.\n * Derive both this and `utmParams` with `extractTrackedParams(searchParams)`\n * — the Referer header never carries the landing page's own query string, so\n * without these two fields SSR-minted sessions have no campaign attribution\n * until the client SDK hydrates.\n */\n clickIds?: Record<string, string>;\n /**\n * Set true when the request carries a tracking opt-out — `DNT: 1` or\n * `Sec-GPC: 1`. When set, the SSR helpers skip the session upsert and the\n * assign/decide call entirely: the page renders defaults/baseline and no\n * session row is minted for the visitor (audit P4). The client SDK already\n * no-ops for these visitors; this keeps the server from minting a session +\n * assignment on every page view before the client can.\n */\n doNotTrack?: boolean;\n /**\n * Declared persona — the role your app already knows for this visitor\n * (e.g. from your auth context during SSR). Same contract as the client\n * option: must be a vocabulary key; unrecognized values are ignored\n * server-side. Never a user id or email.\n */\n persona?: string;\n /**\n * Milliseconds to wait for the API before returning default variants.\n * Prevents slow/cold API from blocking SSR. Defaults to 1000 — the hot path\n * is served from in-process caches and typically returns in well under\n * 150 ms. The full 1 s budget is only reached on a cold start or an API\n * geographically distant from your SSR host, after which defaults render\n * with no layout shift. Lower it if your API is co-located and warm.\n */\n timeoutMs?: number;\n};\n\n/** componentId → assigned variantId */\nexport type ServerAssignments = Record<string, string>;\n\ntype AssignResult = { variantId: string; assignmentTtlMs: number };\n\nconst DEFAULT_TIMEOUT_MS = 1000;\n\nfunction fetchWithTimeout(\n url: string,\n init: RequestInit,\n timeoutMs: number,\n): Promise<Response> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n return fetch(url, { ...init, signal: controller.signal }).finally(() =>\n clearTimeout(timer),\n );\n}\n\n/**\n * Fetches variant assignments server-side for all listed components.\n * Returns a map suitable for passing as `initialAssignments` to `<AdaptiveProvider>`.\n *\n * Call from Next.js layout, Server Component, or getServerSideProps.\n *\n * @example\n * ```tsx\n * const assignments = await preloadAssignments(\n * [\n * { id: 'hero', variantIds: ['hero-a', 'hero-b'] },\n * { id: 'cta', variantIds: ['cta-short', 'cta-long'] },\n * ],\n * sessionId, // read from cookies() or req.cookies\n * { apiKey: process.env.NEXT_PUBLIC_SENTIENT_API_KEY!, baseUrl: 'https://api.sentient-ui.com/v1' },\n * );\n * return <AdaptiveProvider ... initialAssignments={assignments}>{children}</AdaptiveProvider>;\n * ```\n */\nexport async function preloadAssignments(\n components: Array<{ id: string; variantIds: string[] }>,\n sessionId: string,\n config: ServerAssignConfig,\n): Promise<ServerAssignments> {\n // Opted-out visitor (DNT/GPC): render defaults, mint no session (audit P4).\n if (config.doNotTrack) return {};\n\n const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${config.apiKey}`,\n };\n if (config.origin) {\n headers.Origin = config.origin;\n }\n\n // Session metadata must match the browser SDK so assign seeds variant_weights\n // under the same segment (not `unknown:unknown`).\n const sessionBody = {\n ...buildSessionUpsertPayload(sessionId, {\n userAgent: config.userAgent,\n referer: config.referer,\n utmParams: config.utmParams,\n clickIds: config.clickIds,\n appOrigin: config.origin,\n }),\n ...(config.persona ? { persona: config.persona } : {}),\n };\n\n try {\n const res = await fetchWithTimeout(\n `${config.baseUrl}/sessions`,\n { method: 'POST', headers, body: JSON.stringify(sessionBody) },\n timeoutMs,\n );\n if (res.status === 402) {\n console.warn(\n '[SentientUI] Session limit exceeded for this project. The bandit will stop learning until the limit resets. Upgrade at sentient-ui.com/pricing',\n );\n } else if (!res.ok) {\n const body = await res.json().catch(() => ({}));\n console.error(`[SentientUI] preloadAssignments: session upsert failed (${res.status})`, body);\n }\n } catch (err) {\n console.error('[SentientUI] preloadAssignments: session upsert threw', err);\n }\n\n const results = await Promise.allSettled(\n components.map(async ({ id, variantIds }) => {\n const res = await fetchWithTimeout(\n `${config.baseUrl}/assign`,\n { method: 'POST', headers, body: JSON.stringify({ sessionId, componentId: id, variantIds }) },\n timeoutMs,\n );\n if (!res.ok) {\n const body = await res.json().catch(() => ({}));\n console.error(`[SentientUI] preloadAssignments: assign failed for \"${id}\" (${res.status})`, body);\n return null;\n }\n const body = (await res.json()) as AssignResult;\n return { id, variantId: body.variantId };\n }),\n );\n\n const assignments: ServerAssignments = {};\n for (const result of results) {\n if (result.status === 'fulfilled' && result.value) {\n assignments[result.value.id] = result.value.variantId;\n }\n }\n return assignments;\n}\n\n/**\n * Reads the Sentient session cookie from a Next.js `ReadonlyRequestCookies` object\n * (the return value of `cookies()` from `next/headers`), or any object with a\n * `get(name: string)` method. Returns null if the cookie is absent.\n *\n * Pass the project's `apiKey`: the client writes the per-project SUFFIXED name\n * (`sessionCookieName` — see storage-key.ts), so reading only the bare\n * `_snt_uid` missed the cookie on every SSR request for a returning visitor and\n * minted a fresh orphan session each page view (quota inflation, broken sticky\n * assignments and persona continuity). The bare name is still read as a\n * fallback for identities minted before namespacing.\n */\nexport function readSessionCookie(\n cookies: { get(name: string): { value: string } | undefined },\n apiKey?: string,\n): string | null {\n return (\n (apiKey ? cookies.get(sessionCookieName(apiKey))?.value : undefined) ??\n cookies.get(LEGACY_SESSION_COOKIE_NAME)?.value ??\n null\n );\n}\n\nexport type DecideResult = {\n layoutOrder: string[];\n assignments: Record<string, string>;\n /** Slot results keyed by slot id. Baseline-resolved when the API omits/fails them. */\n slots: Record<string, SlotResult>;\n persona: string;\n confidence: number;\n};\n\n/**\n * Single-roundtrip SSR call returning layout order, component assignments,\n * and adaptive-slot results. Falls back to default section order + empty\n * assignments + baseline slots if the API is unavailable. A response without\n * a `slots` field means the server predates slots — every declared slot\n * resolves to its baseline (no retry).\n */\nexport async function preloadDecisions(\n params: {\n sections?: string[];\n components: Array<{ id: string; variantIds?: string[] }>;\n slots?: SlotDeclInput[];\n },\n sessionId: string,\n config: ServerAssignConfig,\n): Promise<DecideResult> {\n const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const declaredSlots = params.slots ?? [];\n const fallback: DecideResult = {\n layoutOrder: params.sections ?? [],\n assignments: {},\n slots: baselineSlots(declaredSlots),\n persona: 'unknown',\n confidence: 0,\n };\n\n // Opted-out visitor (DNT/GPC): baseline layout/slots, mint no session (audit P4).\n if (config.doNotTrack) return fallback;\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${config.apiKey}`,\n };\n if (config.origin) headers.Origin = config.origin;\n\n const sessionBody = {\n ...buildSessionUpsertPayload(sessionId, {\n userAgent: config.userAgent,\n referer: config.referer,\n utmParams: config.utmParams,\n clickIds: config.clickIds,\n appOrigin: config.origin,\n }),\n ...(config.persona ? { persona: config.persona } : {}),\n };\n\n try {\n const sessionRes = await fetchWithTimeout(\n `${config.baseUrl}/sessions`,\n { method: 'POST', headers, body: JSON.stringify(sessionBody) },\n timeoutMs,\n );\n if (!sessionRes.ok) {\n const body = await sessionRes.json().catch(() => ({}));\n console.error(`[SentientUI] preloadDecisions: session upsert failed (${sessionRes.status})`, body);\n }\n } catch (err) {\n console.error('[SentientUI] preloadDecisions: session upsert threw', err);\n }\n\n try {\n const res = await fetchWithTimeout(\n `${config.baseUrl}/decide`,\n {\n method: 'POST',\n headers,\n body: JSON.stringify({\n sessionId,\n sections: (params.sections ?? []).map((id) => ({ id })),\n components: params.components,\n ...(declaredSlots.length > 0 ? { slots: declaredSlots.map(toWireSlot) } : {}),\n ...(config.persona ? { persona: config.persona } : {}),\n }),\n },\n timeoutMs,\n );\n if (!res.ok) {\n const body = await res.json().catch(() => ({}));\n console.error(`[SentientUI] preloadDecisions: decide failed (${res.status})`, body);\n return fallback;\n }\n const data = (await res.json()) as {\n layoutOrder?: string[];\n assignments?: Record<string, string>;\n slots?: Record<string, SlotResult>;\n persona?: string;\n confidence?: number;\n };\n\n const slots: Record<string, SlotResult> = {};\n for (const d of declaredSlots) {\n // `data.slots === undefined` ⇒ server predates slots: baseline, no retry.\n slots[d.id] = data.slots?.[d.id] ?? baselineResultFor(d);\n }\n\n return {\n layoutOrder: data.layoutOrder ?? params.sections ?? [],\n assignments: data.assignments ?? {},\n slots,\n persona: data.persona ?? 'unknown',\n confidence: data.confidence ?? 0,\n };\n } catch (err) {\n console.error('[SentientUI] preloadDecisions: decide threw', err);\n return fallback;\n }\n}\n"],"mappings":"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","names":["DEFAULT_TIMEOUT_MS","fetchWithTimeout","url","init","timeoutMs","controller","timer","__spreadProps","__spreadValues","preloadAssignments","components","sessionId","config","_a","headers","sessionBody","buildSessionUpsertPayload","res","body","err","results","id","variantIds","assignments","result","readSessionCookie","cookies","apiKey","_b","_c","_d","sessionCookieName","LEGACY_SESSION_COOKIE_NAME","preloadDecisions","params","_e","_f","_g","_h","_i","_j","_k","declaredSlots","fallback","baselineSlots","sessionRes","toWireSlot","data","slots","d","baselineResultFor"]}
@@ -0,0 +1,21 @@
1
+ import { T as TopicRule, a as SectionFeatures } from './classify-DYlSjkFP.cjs';
2
+ export { b as SemanticType } from './classify-DYlSjkFP.cjs';
3
+
4
+ declare const CLASSIFIER_TOPICS: readonly TopicRule[];
5
+ /** Which classifier stage produced the label. Persisted as provenance (the
6
+ * `page_semantics.source` column distinguishes structured_data from
7
+ * heuristic), so it must name the stage that actually MATCHED — a section
8
+ * that merely CONTAINS a JSON-LD blob but was classified by its `<nav>` tag
9
+ * is heuristic evidence, not the site telling us what it is. */
10
+ type ClassifierStage = 'structural' | 'structured_data' | 'keyword' | 'content' | 'fallback';
11
+ /**
12
+ * Classify a section into a topic, its parent, and its functional role.
13
+ * Structure and fallbacks are the shared implementation from ./classify.ts, so
14
+ * the browser cannot disagree with the server about what a `<nav>` is.
15
+ */
16
+ declare function classifyTopic(f: SectionFeatures): TopicRule & {
17
+ strength: 'strong' | 'weak';
18
+ stage: ClassifierStage;
19
+ };
20
+
21
+ export { CLASSIFIER_TOPICS, SectionFeatures, TopicRule, classifyTopic };
@@ -0,0 +1,21 @@
1
+ import { T as TopicRule, a as SectionFeatures } from './classify-DYlSjkFP.js';
2
+ export { b as SemanticType } from './classify-DYlSjkFP.js';
3
+
4
+ declare const CLASSIFIER_TOPICS: readonly TopicRule[];
5
+ /** Which classifier stage produced the label. Persisted as provenance (the
6
+ * `page_semantics.source` column distinguishes structured_data from
7
+ * heuristic), so it must name the stage that actually MATCHED — a section
8
+ * that merely CONTAINS a JSON-LD blob but was classified by its `<nav>` tag
9
+ * is heuristic evidence, not the site telling us what it is. */
10
+ type ClassifierStage = 'structural' | 'structured_data' | 'keyword' | 'content' | 'fallback';
11
+ /**
12
+ * Classify a section into a topic, its parent, and its functional role.
13
+ * Structure and fallbacks are the shared implementation from ./classify.ts, so
14
+ * the browser cannot disagree with the server about what a `<nav>` is.
15
+ */
16
+ declare function classifyTopic(f: SectionFeatures): TopicRule & {
17
+ strength: 'strong' | 'weak';
18
+ stage: ClassifierStage;
19
+ };
20
+
21
+ export { CLASSIFIER_TOPICS, SectionFeatures, TopicRule, classifyTopic };
@@ -0,0 +1,2 @@
1
+ "use strict";var p=Object.defineProperty,y=Object.defineProperties,h=Object.getOwnPropertyDescriptor,w=Object.getOwnPropertyDescriptors,v=Object.getOwnPropertyNames,l=Object.getOwnPropertySymbols;var g=Object.prototype.hasOwnProperty,T=Object.prototype.propertyIsEnumerable;var b=(e,r,t)=>r in e?p(e,r,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[r]=t,n=(e,r)=>{for(var t in r||(r={}))g.call(r,t)&&b(e,t,r[t]);if(l)for(var t of l(r))T.call(r,t)&&b(e,t,r[t]);return e},i=(e,r)=>y(e,w(r));var S=(e,r)=>{for(var t in r)p(e,t,{get:r[t],enumerable:!0})},x=(e,r,t,s)=>{if(r&&typeof r=="object"||typeof r=="function")for(let o of v(r))!g.call(e,o)&&o!==t&&p(e,o,{get:()=>r[o],enumerable:!(s=h(r,o))||s.enumerable});return e};var _=e=>x(p({},"__esModule",{value:!0}),e);var E={};S(E,{CLASSIFIER_TOPICS:()=>u,classifyTopic:()=>m});module.exports=_(E);var k={banner:"hero",navigation:"navigation",contentinfo:"footer"};function d(e){let r=e.ariaRole?k[e.ariaRole.toLowerCase()]:void 0;if(r)return r;if(e.tag==="nav")return"navigation";if(e.tag==="footer")return"footer";if(e.tag==="header")return"hero";let t=`${e.idClass} ${e.headingText}`.toLowerCase();return/\b(navbar|nav-bar|navigation|site-nav|main-nav|topbar|footer)\b/.test(t)?/footer/.test(t)?"footer":"navigation":/\b(hero|masthead|jumbotron)\b/i.test(e.idClass)?"hero":null}function f(e){return e.actionCount>=1&&e.textLength>0&&e.textLength<200?"cta":"generic"}var u=[{topic:"navigation",parent:"navigation",role:"structural"},{topic:"footer",parent:"navigation",role:"structural"},{topic:"breadcrumb",parent:"navigation",role:"structural"},{topic:"cookie_banner",parent:"navigation",role:"structural"},{topic:"hero",parent:"hero",role:"converter"},{topic:"pricing_plans",parent:"pricing",role:"converter"},{topic:"quote",parent:"pricing",role:"converter"},{topic:"financing",parent:"pricing",role:"persuader"},{topic:"faq",parent:"faq",role:"persuader"},{topic:"comparison",parent:"comparison",role:"persuader"},{topic:"reviews",parent:"social_proof",role:"persuader"},{topic:"testimonials",parent:"social_proof",role:"persuader"},{topic:"case_study",parent:"social_proof",role:"persuader"},{topic:"brands",parent:"social_proof",role:"persuader"},{topic:"awards",parent:"social_proof",role:"persuader"},{topic:"gallery",parent:"social_proof",role:"persuader"},{topic:"stats",parent:"social_proof",role:"persuader"},{topic:"insurance",parent:"trust",role:"persuader"},{topic:"warranty",parent:"trust",role:"persuader"},{topic:"certifications",parent:"trust",role:"persuader"},{topic:"security",parent:"trust",role:"persuader"},{topic:"guarantee",parent:"trust",role:"persuader"},{topic:"about",parent:"trust",role:"persuader"},{topic:"team",parent:"trust",role:"persuader"},{topic:"services",parent:"features",role:"persuader"},{topic:"features",parent:"features",role:"persuader"},{topic:"process",parent:"features",role:"persuader"},{topic:"menu",parent:"features",role:"persuader"},{topic:"inventory",parent:"features",role:"converter"},{topic:"integrations",parent:"features",role:"persuader"},{topic:"booking",parent:"cta",role:"converter"},{topic:"contact_form",parent:"cta",role:"converter"},{topic:"newsletter",parent:"cta",role:"converter"},{topic:"hours",parent:"cta",role:"persuader"},{topic:"location",parent:"cta",role:"persuader"},{topic:"cta",parent:"cta",role:"converter"},{topic:"blog",parent:"generic",role:"persuader"},{topic:"careers",parent:"generic",role:"persuader"},{topic:"legal",parent:"generic",role:"persuader"},{topic:"generic",parent:"generic",role:"persuader"}],C=new Map(u.map(e=>[e.topic,e]));function c(e){var r;return(r=C.get(e))!=null?r:{topic:"generic",parent:"generic",role:"persuader"}}var R=[["cookie_banner",/\bcookies?\b[^.]{0,20}\b(banner|notice|consent|policy)\b|consent banner/i],["breadcrumb",/\bbreadcrumbs?\b/i],["insurance",/\b(insurance|insurer|insured)\b/i],["warranty",/\b(warrant(y|ies)|guarantees?|guaranteed)\b/i],["certifications",/\b(certifi|accredit|licen[cs]ed)\b|approved by|iso ?\d/i],["security",/\b(security|privacy|compliance|gdpr|encrypt)\b|soc ?2/i],["about",/why choose|why us|about us|our story|who we are|years? (of )?(experience|serving)/i],["team",/our team|meet the team|our people|our staff/i],["reviews",/\b(reviews?|ratings?|testimonial)\b|what our customers say/i],["brands",/\b(brands?|partners?|manufacturers?|logos)\b|we work with|trusted by|loved by/i],["gallery",/\b(galler(y|ies)|photos?)\b|before and after/i],["case_study",/case stud|selected work|portfolio|our projects?|recent work/i],["awards",/\b(awards?|accolades?)\b/i],["pricing_plans",/\b(pricing|price list|per month|\/mo|subscriptions?)\b|\bplans?\b(?=[^.]{0,40}(from|start|month|year|[$€£]))/i],["quote",/get a quote|request a quote|free estimate|get an estimate/i],["financing",/\b(financ(e|ing)|instal?ments?)\b|payment plan|pay monthly/i],["booking",/\b(book|booking|reserve|reservation|appointments?)\b|schedule a/i],["newsletter",/\b(newsletter|subscribe)\b|join our (list|mailing)|mailing list/i],["contact_form",/contact us|get in touch|start a project|\benquir|\binquir|send us/i],["hours",/opening hours|\bhours\b|open (mon|tue|wed|thu|fri|sat|sun)/i],["location",/find us|our location|directions|where we are|visit us/i],["menu",/\b(menus?|dishes)\b|our food/i],["inventory",/\bbest ?sellers?\b|\b(shop|products?|collection)\b|our range|in stock/i],["process",/our process|how it works|the process|what to expect|\bsteps?\b/i],["services",/\b(services?|repairs?|treatments?|specialit|specialt)\b|what we (do|offer)/i],["integrations",/\bintegrations?\b|works with|connects? to/i],["features",/\b(features?|benefits?|capabilit)\b|what you get/i],["faq",/\bfaq\b|frequently asked|common questions?/i],["comparison",/\b(compare|comparison|versus)\b|\bvs\./i],["blog",/\b(blog|news|articles?|insights?)\b/i],["careers",/\b(careers?|jobs?)\b|join (the|our) team|we.re hiring/i],["legal",/\bterms\b|privacy policy|cookie policy|\blegal\b/i]],q={FAQPage:"faq",Question:"faq",Review:"reviews",AggregateRating:"reviews",Offer:"pricing_plans",AggregateOffer:"pricing_plans",PriceSpecification:"pricing_plans",Service:"services",Product:"inventory",ItemList:"inventory",Menu:"menu",MenuSection:"menu",LocalBusiness:"location",PostalAddress:"location",OpeningHoursSpecification:"hours",BreadcrumbList:"breadcrumb",ImageGallery:"gallery",JobPosting:"careers",BlogPosting:"blog",Article:"blog",Organization:"about"},A=[["pricing_plans",/(?:[$€£]\s?\d[\d,.]*\s*(?:\/|per\s)\s*(?:mo|month|yr|year|seat|user))|(?:\b(?:starter|basic|pro|growth|premium|enterprise)\b[^.]{0,60}[$€£]\s?\d)/i],["reviews",/(?:★{2,})|(?:\b\d(?:\.\d)?\s*(?:out of|\/)\s*5\b)|(?:\brated\b)|(?:["“][^"”]{20,160}["”]\s*[—–-]\s*[A-Z][a-z]+)/],["guarantee",/\b(?:money[- ]back guarantee|free returns?|returns? within|refund(?:s)? within|cancel anytime)\b/i],["security",/\b(?:encrypted|soc ?2|iso ?27001|gdpr[- ]compliant)\b/i],["comparison",/\b(?:vs|versus)\b\.?[^.!?]{0,80}\b(?:compare|comparison|plans?|features?|alternative)\b|\bhow (?:we|it) compares?\b/i]];function m(e){var s;let r=d(e);if(r)return i(n({},c(r)),{strength:"strong",stage:"structural"});for(let o of(s=e.structuredTypes)!=null?s:[]){let a=q[o];if(a)return i(n({},c(a)),{strength:"strong",stage:"structured_data"})}let t=`${e.idClass} ${e.headingText}`.toLowerCase();for(let[o,a]of R)if(a.test(t))return i(n({},c(o)),{strength:"strong",stage:"keyword"});for(let[o,a]of A)if(a.test(e.bodyText))return i(n({},c(o)),{strength:"strong",stage:"content"});return i(n({},c(f(e))),{strength:"weak",stage:"fallback"})}0&&(module.exports={CLASSIFIER_TOPICS,classifyTopic});
2
+ //# sourceMappingURL=index-topics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index-topics.ts","../src/engagement/classify.ts","../src/engagement/topics.ts"],"sourcesContent":["// SERVER-ONLY entry for the rich section-topic vocabulary.\n//\n// Separate from ./engagement on purpose: the topic tables cost ~1.5 KB gzip,\n// which is 7% of the always-on snippet, on every page view of every customer\n// site — and the browser never reads them (classifySection returns only the\n// parent). Only the site-audit classification job imports this.\nexport {\n classifyTopic,\n CLASSIFIER_TOPICS,\n type TopicRule,\n} from './engagement/topics';\nexport type { SectionFeatures, SemanticType } from './engagement/classify';\n","// Semantic section classification for no-code section capture (Phase 3 §2.4).\n// Pure heuristic: element → one of the graph_nodes semantic_type enum. Mirrors\n// the SDK graph scanner's vocabulary so the persona × section matrix consumes\n// snippet-captured sections unchanged.\n//\n// SPLIT (Phase 2b, spec 2026-09-04). This module is the BROWSER-SAFE core:\n// structural rules, the small parent-level keyword table, and the fallbacks.\n// The rich ~40-topic vocabulary lives in ./topics.ts, which only the server\n// imports — shipping it here cost 1.5 KB gzip in every bundle (7% of the\n// always-on snippet) for a layer the browser never reads, since classifySection\n// returns only the parent.\n//\n// Both paths share structuralTopicOf/fallbackTopicOf, so the three fixes below\n// apply identically on client and server; only vocabulary richness differs.\n//\n// Fixes, each reproduced against the shipped classifier before being changed:\n// 1. structural before the converter fallback — `<div class=\"navbar\">` used\n// to become `cta`, because `navigation` was reachable only via tag\n// nav/footer.\n// 2. hero before the converter fallback — the cta rule sat above both hero\n// rules, so a `<header>` with a button and <200 chars could never be hero.\n// 3. tightened pricing/social keywords — `plans?` and `customers?` fired on\n// unrelated copy AND were `strong`, so they auto-applied at confidence 0.9\n// and were never sent to the LLM fallback.\n\nexport type SemanticType =\n | 'pricing' | 'hero' | 'social_proof' | 'cta' | 'features'\n | 'faq' | 'comparison' | 'trust' | 'navigation' | 'generic';\n\nexport type SectionRole = 'converter' | 'persuader' | 'structural';\n\n/** Canonical vocabulary — mirrors the graph_nodes semantic_type CHECK enum. */\nexport const SEMANTIC_TYPES: readonly SemanticType[] = [\n 'pricing', 'hero', 'social_proof', 'cta', 'features',\n 'faq', 'comparison', 'trust', 'navigation', 'generic',\n] as const;\n\nexport type TopicRule = { topic: string; parent: SemanticType; role: SectionRole };\n\n/** Environment-agnostic section features — buildable from a browser Element or\n * a server-parsed node (node-html-parser). */\nexport type SectionFeatures = {\n tag: string; // lowercase tag name\n idClass: string; // `${id} ${className}`\n headingText: string;\n bodyText: string; // normalized text content, first 2000 chars\n actionCount: number;\n textLength: number;\n /** ARIA landmark role, when the element declares one. Optional so every\n * existing caller keeps compiling; a page that uses landmarks gives a\n * high-precision signal for free, which the old classifier ignored — it\n * tested membership of SEMANTIC_TYPES, so only role=\"navigation\" ever hit\n * and role=\"banner\"/\"contentinfo\" were discarded. */\n ariaRole?: string;\n /** schema.org `@type` values found in `<script type=\"application/ld+json\">`\n * INSIDE this section. Highest-precision signal available and free to\n * collect — the crawler already has the HTML. Type-only here; the\n * `@type` → topic table is server-side in ./topics.ts, since the browser\n * never reads the topic layer and the snippet has ~200 bytes of margin. */\n structuredTypes?: string[];\n};\n\n// ARIA landmark → topic. Structural facts, not guesses.\nconst ARIA_TOPIC: Record<string, string> = {\n banner: 'hero',\n navigation: 'navigation',\n contentinfo: 'footer',\n};\n\n/**\n * Structural identification, shared by the browser and server classifiers.\n * Returns a topic name, or null when the section is not structural furniture.\n * Everything matched here is definitive (tag or authored marker), so callers\n * treat it as `strong`.\n */\nexport function structuralTopicOf(f: SectionFeatures): string | null {\n const aria = f.ariaRole ? ARIA_TOPIC[f.ariaRole.toLowerCase()] : undefined;\n if (aria) return aria;\n if (f.tag === 'nav') return 'navigation';\n if (f.tag === 'footer') return 'footer';\n // A page-level <header> IS the banner landmark (HTML-AAM maps it to\n // role=\"banner\", which this function already treats as hero), so it is a\n // structural fact rather than a keyword guess. Without this, a header whose\n // headline happens to contain a content word loses to the keyword table:\n // \"We build brands that move\" scored social_proof and \"Winter collection\"\n // scored features, purely on words inside the hero copy.\n if (f.tag === 'header') return 'hero';\n const hay = `${f.idClass} ${f.headingText}`.toLowerCase();\n if (/\\b(navbar|nav-bar|navigation|site-nav|main-nav|topbar|footer)\\b/.test(hay)) {\n return /footer/.test(hay) ? 'footer' : 'navigation';\n }\n // Explicit authoring marker beats any inferred keyword, and is matched on\n // idClass ONLY: a `<header class=\"hero\">` headlined \"Expert Car Repair\" is a\n // hero, not a services section — but matching \"hero\" in heading text would\n // also catch \"Hero of the story\".\n if (/\\b(hero|masthead|jumbotron)\\b/i.test(f.idClass)) return 'hero';\n return null;\n}\n\n/**\n * Last-resort classification once no keyword or content evidence matched.\n * Hero is checked BEFORE the converter fallback — fix 2 above.\n */\nexport function fallbackTopicOf(f: SectionFeatures): string {\n // `tag === 'header'` is handled in structuralTopicOf, which runs first.\n if (f.actionCount >= 1 && f.textLength > 0 && f.textLength < 200) return 'cta';\n return 'generic';\n}\n\n// Parent-level keyword table for the BROWSER path. Deliberately close to the\n// original size — the rich topic vocabulary is in ./topics.ts. `plans?` now\n// requires adjacent pricing context, and bare `customers?` is gone (it fired on\n// navigation furniture like a \"Customer Service\" footer block).\nconst KEYWORDS: Array<[SemanticType, RegExp]> = [\n ['pricing', /\\b(pricing|price list|per month|\\/mo|subscriptions?)\\b|\\bplans?\\b(?=[^.]{0,40}(from|start|month|year|[$€£]))/i],\n ['faq', /\\bfaq\\b|frequently asked|common questions?/i],\n ['comparison', /\\b(compare|comparison|versus)\\b|\\bvs\\./i],\n ['social_proof', /\\b(reviews?|ratings?|testimonial|brands?|galler(y|ies)|logos)\\b|trusted by|loved by|case stud|what our customers say/i],\n ['trust', /\\b(insurance|warrant(y|ies)|guarantees?|certifi|accredit|security|privacy|compliance|gdpr|encrypt)\\b|why choose|about us|our team/i],\n ['cta', /\\b(book|booking|reserve|appointments?|newsletter|subscribe)\\b|contact us|get in touch|opening hours/i],\n ['features', /\\b(features?|benefits?|capabilit|services?|repairs?|menus?|products?)\\b|how it works|our process|what we (do|offer)|what you get/i],\n];\n\n// Content-evidence patterns — run against bodyText when the heading gave us\n// nothing. Deliberately conservative: pricing needs per-period/plan context next\n// to money so an article mentioning \"$5 million\" stays generic.\nexport const CONTENT_PATTERNS: Array<[SemanticType, RegExp]> = [\n ['pricing', /(?:[$€£]\\s?\\d[\\d,.]*\\s*(?:\\/|per\\s)\\s*(?:mo|month|yr|year|seat|user))|(?:\\b(?:starter|basic|pro|growth|premium|enterprise)\\b[^.]{0,60}[$€£]\\s?\\d)/i],\n ['social_proof', /(?:★{2,})|(?:\\b\\d(?:\\.\\d)?\\s*(?:out of|\\/)\\s*5\\b)|(?:\\brated\\b)|(?:[\"“][^\"”]{20,160}[\"”]\\s*[—–-]\\s*[A-Z][a-z]+)/],\n ['trust', /\\b(?:money[- ]back guarantee|free returns?|returns? within|refund(?:s)? within|encrypted|soc ?2|iso ?27001|gdpr[- ]compliant|cancel anytime)\\b/i],\n ['comparison', /\\b(?:vs|versus)\\b\\.?[^.!?]{0,80}\\b(?:compare|comparison|plans?|features?|alternative)\\b|\\bhow (?:we|it) compares?\\b/i],\n];\n\n/** Parent for the handful of topics the shared structural/fallback helpers\n * emit. The server maps the full vocabulary via CLASSIFIER_TOPICS instead. */\nconst SHARED_TOPIC_PARENT: Record<string, SemanticType> = {\n navigation: 'navigation',\n footer: 'navigation',\n hero: 'hero',\n cta: 'cta',\n generic: 'generic',\n};\n\nfunction headingText(el: Element): string {\n const h = el.querySelector('h1, h2, h3');\n return (h?.textContent ?? '').slice(0, 160);\n}\n\n/**\n * Pure classification over extracted features. `strong` = keyword, content, or\n * structural evidence (trustable enough to auto-apply); `weak` = a fallback\n * guess (hero/cta/generic — capture-worthy but not persona evidence).\n */\nexport function classifyFeatures(f: SectionFeatures): { type: SemanticType; strength: 'strong' | 'weak' } {\n const structural = structuralTopicOf(f);\n if (structural) return { type: SHARED_TOPIC_PARENT[structural] ?? 'generic', strength: 'strong' };\n\n const hay = `${f.idClass} ${f.headingText}`.toLowerCase();\n for (const [type, re] of KEYWORDS) if (re.test(hay)) return { type, strength: 'strong' };\n for (const [type, re] of CONTENT_PATTERNS) if (re.test(f.bodyText)) return { type, strength: 'strong' };\n\n const fb = fallbackTopicOf(f);\n return { type: SHARED_TOPIC_PARENT[fb] ?? 'generic', strength: 'weak' };\n}\n\n/** Feature extraction from a live DOM element (browser paths). */\nexport function featuresFromElement(el: Element): SectionFeatures {\n const text = (el.textContent ?? '').replace(/\\s+/g, ' ').trim();\n const role = el.getAttribute('role');\n return {\n tag: el.tagName.toLowerCase(),\n idClass: `${el.id} ${String(el.className ?? '')}`,\n headingText: headingText(el),\n bodyText: text.slice(0, 2000),\n actionCount: el.querySelectorAll('a, button, [role=\"button\"]').length,\n textLength: text.length,\n ...(role ? { ariaRole: role } : {}),\n };\n}\n\n/** Classify a page section into a semantic type (never null — falls back to\n * 'generic' so the caller can still capture attention on it). */\nexport function classifySection(el: Element): SemanticType {\n return classifyFeatures(featuresFromElement(el)).type;\n}\n","// Rich topic vocabulary for section classification (Phase 2b, spec 2026-09-04).\n//\n// SERVER-ONLY. This module is deliberately NOT imported by ./classify.ts, which\n// the browser bundles: the tables below cost ~1.5 KB gzip, which is 7% of the\n// always-on snippet, on every page view of every customer site — for a layer\n// the browser never reads (classifySection returns only the parent).\n//\n// It shares structuralTopicOf/fallbackTopicOf with ./classify.ts, so client and\n// server agree on structure and fallbacks; only vocabulary richness differs.\n//\n// The topic→parent mapping is DUPLICATED from packages/policy/src/taxonomy.ts,\n// for the same bundle reason — core must not import the policy taxonomy.\n// apps/api/src/domain/classifier-taxonomy-sync.test.ts fails if the two drift.\n\nimport {\n structuralTopicOf,\n fallbackTopicOf,\n type SectionFeatures,\n type SemanticType,\n type TopicRule,\n} from './classify.js';\n\nexport const CLASSIFIER_TOPICS: readonly TopicRule[] = [\n { topic: 'navigation', parent: 'navigation', role: 'structural' },\n { topic: 'footer', parent: 'navigation', role: 'structural' },\n { topic: 'breadcrumb', parent: 'navigation', role: 'structural' },\n { topic: 'cookie_banner', parent: 'navigation', role: 'structural' },\n { topic: 'hero', parent: 'hero', role: 'converter' },\n { topic: 'pricing_plans', parent: 'pricing', role: 'converter' },\n { topic: 'quote', parent: 'pricing', role: 'converter' },\n { topic: 'financing', parent: 'pricing', role: 'persuader' },\n { topic: 'faq', parent: 'faq', role: 'persuader' },\n { topic: 'comparison', parent: 'comparison', role: 'persuader' },\n { topic: 'reviews', parent: 'social_proof', role: 'persuader' },\n { topic: 'testimonials', parent: 'social_proof', role: 'persuader' },\n { topic: 'case_study', parent: 'social_proof', role: 'persuader' },\n { topic: 'brands', parent: 'social_proof', role: 'persuader' },\n { topic: 'awards', parent: 'social_proof', role: 'persuader' },\n { topic: 'gallery', parent: 'social_proof', role: 'persuader' },\n { topic: 'stats', parent: 'social_proof', role: 'persuader' },\n { topic: 'insurance', parent: 'trust', role: 'persuader' },\n { topic: 'warranty', parent: 'trust', role: 'persuader' },\n { topic: 'certifications', parent: 'trust', role: 'persuader' },\n { topic: 'security', parent: 'trust', role: 'persuader' },\n { topic: 'guarantee', parent: 'trust', role: 'persuader' },\n { topic: 'about', parent: 'trust', role: 'persuader' },\n { topic: 'team', parent: 'trust', role: 'persuader' },\n { topic: 'services', parent: 'features', role: 'persuader' },\n { topic: 'features', parent: 'features', role: 'persuader' },\n { topic: 'process', parent: 'features', role: 'persuader' },\n { topic: 'menu', parent: 'features', role: 'persuader' },\n { topic: 'inventory', parent: 'features', role: 'converter' },\n { topic: 'integrations', parent: 'features', role: 'persuader' },\n { topic: 'booking', parent: 'cta', role: 'converter' },\n { topic: 'contact_form', parent: 'cta', role: 'converter' },\n { topic: 'newsletter', parent: 'cta', role: 'converter' },\n { topic: 'hours', parent: 'cta', role: 'persuader' },\n { topic: 'location', parent: 'cta', role: 'persuader' },\n { topic: 'cta', parent: 'cta', role: 'converter' },\n { topic: 'blog', parent: 'generic', role: 'persuader' },\n { topic: 'careers', parent: 'generic', role: 'persuader' },\n { topic: 'legal', parent: 'generic', role: 'persuader' },\n { topic: 'generic', parent: 'generic', role: 'persuader' },\n];\n\nconst RULE = new Map(CLASSIFIER_TOPICS.map((t) => [t.topic, t]));\n\nfunction ruleFor(topic: string): TopicRule {\n return RULE.get(topic) ?? { topic: 'generic', parent: 'generic', role: 'persuader' };\n}\n\n// ORDER IS LOAD-BEARING — first hit wins, so specific must precede general:\n// * `insurance` precedes `services`, or \"Insurance Approved Repairs\" becomes\n// a services section on the strength of \"repairs\".\n// * `brands` precedes `case_study`, or \"Brands We Work With\" matches \"work\".\n// * `pricing_plans` requires real pricing context — see fix 3 in ./classify.ts.\nconst TOPIC_KEYWORDS: Array<[string, RegExp]> = [\n ['cookie_banner', /\\bcookies?\\b[^.]{0,20}\\b(banner|notice|consent|policy)\\b|consent banner/i],\n ['breadcrumb', /\\bbreadcrumbs?\\b/i],\n ['insurance', /\\b(insurance|insurer|insured)\\b/i],\n ['warranty', /\\b(warrant(y|ies)|guarantees?|guaranteed)\\b/i],\n ['certifications', /\\b(certifi|accredit|licen[cs]ed)\\b|approved by|iso ?\\d/i],\n ['security', /\\b(security|privacy|compliance|gdpr|encrypt)\\b|soc ?2/i],\n ['about', /why choose|why us|about us|our story|who we are|years? (of )?(experience|serving)/i],\n ['team', /our team|meet the team|our people|our staff/i],\n ['reviews', /\\b(reviews?|ratings?|testimonial)\\b|what our customers say/i],\n ['brands', /\\b(brands?|partners?|manufacturers?|logos)\\b|we work with|trusted by|loved by/i],\n ['gallery', /\\b(galler(y|ies)|photos?)\\b|before and after/i],\n ['case_study', /case stud|selected work|portfolio|our projects?|recent work/i],\n ['awards', /\\b(awards?|accolades?)\\b/i],\n ['pricing_plans', /\\b(pricing|price list|per month|\\/mo|subscriptions?)\\b|\\bplans?\\b(?=[^.]{0,40}(from|start|month|year|[$€£]))/i],\n ['quote', /get a quote|request a quote|free estimate|get an estimate/i],\n ['financing', /\\b(financ(e|ing)|instal?ments?)\\b|payment plan|pay monthly/i],\n ['booking', /\\b(book|booking|reserve|reservation|appointments?)\\b|schedule a/i],\n ['newsletter', /\\b(newsletter|subscribe)\\b|join our (list|mailing)|mailing list/i],\n ['contact_form', /contact us|get in touch|start a project|\\benquir|\\binquir|send us/i],\n ['hours', /opening hours|\\bhours\\b|open (mon|tue|wed|thu|fri|sat|sun)/i],\n ['location', /find us|our location|directions|where we are|visit us/i],\n ['menu', /\\b(menus?|dishes)\\b|our food/i],\n ['inventory', /\\bbest ?sellers?\\b|\\b(shop|products?|collection)\\b|our range|in stock/i],\n ['process', /our process|how it works|the process|what to expect|\\bsteps?\\b/i],\n ['services', /\\b(services?|repairs?|treatments?|specialit|specialt)\\b|what we (do|offer)/i],\n ['integrations', /\\bintegrations?\\b|works with|connects? to/i],\n ['features', /\\b(features?|benefits?|capabilit)\\b|what you get/i],\n ['faq', /\\bfaq\\b|frequently asked|common questions?/i],\n ['comparison', /\\b(compare|comparison|versus)\\b|\\bvs\\./i],\n ['blog', /\\b(blog|news|articles?|insights?)\\b/i],\n ['careers', /\\b(careers?|jobs?)\\b|join (the|our) team|we.re hiring/i],\n ['legal', /\\bterms\\b|privacy policy|cookie policy|\\blegal\\b/i],\n];\n\n/**\n * schema.org `@type` → topic. The highest-precision signal available, and free:\n * the crawler already has the HTML, and a site that publishes structured data\n * has told us what a section is rather than leaving us to infer it from copy.\n *\n * Ranked BELOW structural rules deliberately — many sites emit an Organization\n * or WebSite blob site-wide in the header or footer, and letting that outrank a\n * `<nav>` would turn every such navigation bar into an `about` section.\n */\nconst SCHEMA_TOPIC: Record<string, string> = {\n FAQPage: 'faq',\n Question: 'faq',\n Review: 'reviews',\n AggregateRating: 'reviews',\n Offer: 'pricing_plans',\n AggregateOffer: 'pricing_plans',\n PriceSpecification: 'pricing_plans',\n Service: 'services',\n Product: 'inventory',\n ItemList: 'inventory',\n Menu: 'menu',\n MenuSection: 'menu',\n LocalBusiness: 'location',\n PostalAddress: 'location',\n OpeningHoursSpecification: 'hours',\n BreadcrumbList: 'breadcrumb',\n ImageGallery: 'gallery',\n JobPosting: 'careers',\n BlogPosting: 'blog',\n Article: 'blog',\n Organization: 'about',\n};\n\nconst TOPIC_CONTENT: Array<[string, RegExp]> = [\n ['pricing_plans', /(?:[$€£]\\s?\\d[\\d,.]*\\s*(?:\\/|per\\s)\\s*(?:mo|month|yr|year|seat|user))|(?:\\b(?:starter|basic|pro|growth|premium|enterprise)\\b[^.]{0,60}[$€£]\\s?\\d)/i],\n ['reviews', /(?:★{2,})|(?:\\b\\d(?:\\.\\d)?\\s*(?:out of|\\/)\\s*5\\b)|(?:\\brated\\b)|(?:[\"“][^\"”]{20,160}[\"”]\\s*[—–-]\\s*[A-Z][a-z]+)/],\n ['guarantee', /\\b(?:money[- ]back guarantee|free returns?|returns? within|refund(?:s)? within|cancel anytime)\\b/i],\n ['security', /\\b(?:encrypted|soc ?2|iso ?27001|gdpr[- ]compliant)\\b/i],\n ['comparison', /\\b(?:vs|versus)\\b\\.?[^.!?]{0,80}\\b(?:compare|comparison|plans?|features?|alternative)\\b|\\bhow (?:we|it) compares?\\b/i],\n];\n\n/** Which classifier stage produced the label. Persisted as provenance (the\n * `page_semantics.source` column distinguishes structured_data from\n * heuristic), so it must name the stage that actually MATCHED — a section\n * that merely CONTAINS a JSON-LD blob but was classified by its `<nav>` tag\n * is heuristic evidence, not the site telling us what it is. */\nexport type ClassifierStage = 'structural' | 'structured_data' | 'keyword' | 'content' | 'fallback';\n\n/**\n * Classify a section into a topic, its parent, and its functional role.\n * Structure and fallbacks are the shared implementation from ./classify.ts, so\n * the browser cannot disagree with the server about what a `<nav>` is.\n */\nexport function classifyTopic(\n f: SectionFeatures,\n): TopicRule & { strength: 'strong' | 'weak'; stage: ClassifierStage } {\n const structural = structuralTopicOf(f);\n if (structural) return { ...ruleFor(structural), strength: 'strong', stage: 'structural' };\n\n // Stage 0: the site told us what this is. Beats every inferred keyword.\n for (const t of f.structuredTypes ?? []) {\n const topic = SCHEMA_TOPIC[t];\n if (topic) return { ...ruleFor(topic), strength: 'strong', stage: 'structured_data' };\n }\n\n const hay = `${f.idClass} ${f.headingText}`.toLowerCase();\n for (const [topic, re] of TOPIC_KEYWORDS) {\n if (re.test(hay)) return { ...ruleFor(topic), strength: 'strong', stage: 'keyword' };\n }\n for (const [topic, re] of TOPIC_CONTENT) {\n if (re.test(f.bodyText)) return { ...ruleFor(topic), strength: 'strong', stage: 'content' };\n }\n return { ...ruleFor(fallbackTopicOf(f)), strength: 'weak', stage: 'fallback' };\n}\n\nexport type { SemanticType, SectionFeatures, TopicRule };\n"],"mappings":"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","names":["index_topics_exports","__export","CLASSIFIER_TOPICS","classifyTopic","__toCommonJS","ARIA_TOPIC","structuralTopicOf","f","aria","hay","fallbackTopicOf","CLASSIFIER_TOPICS","RULE","t","ruleFor","topic","_a","TOPIC_KEYWORDS","SCHEMA_TOPIC","TOPIC_CONTENT","classifyTopic","f","structural","structuralTopicOf","__spreadProps","__spreadValues","hay","re","fallbackTopicOf"]}
@@ -0,0 +1,2 @@
1
+ import{b as c,c as p}from"./chunk-EWP6FTHE.mjs";import{a as t,b as o}from"./chunk-TMCGHANO.mjs";var u=[{topic:"navigation",parent:"navigation",role:"structural"},{topic:"footer",parent:"navigation",role:"structural"},{topic:"breadcrumb",parent:"navigation",role:"structural"},{topic:"cookie_banner",parent:"navigation",role:"structural"},{topic:"hero",parent:"hero",role:"converter"},{topic:"pricing_plans",parent:"pricing",role:"converter"},{topic:"quote",parent:"pricing",role:"converter"},{topic:"financing",parent:"pricing",role:"persuader"},{topic:"faq",parent:"faq",role:"persuader"},{topic:"comparison",parent:"comparison",role:"persuader"},{topic:"reviews",parent:"social_proof",role:"persuader"},{topic:"testimonials",parent:"social_proof",role:"persuader"},{topic:"case_study",parent:"social_proof",role:"persuader"},{topic:"brands",parent:"social_proof",role:"persuader"},{topic:"awards",parent:"social_proof",role:"persuader"},{topic:"gallery",parent:"social_proof",role:"persuader"},{topic:"stats",parent:"social_proof",role:"persuader"},{topic:"insurance",parent:"trust",role:"persuader"},{topic:"warranty",parent:"trust",role:"persuader"},{topic:"certifications",parent:"trust",role:"persuader"},{topic:"security",parent:"trust",role:"persuader"},{topic:"guarantee",parent:"trust",role:"persuader"},{topic:"about",parent:"trust",role:"persuader"},{topic:"team",parent:"trust",role:"persuader"},{topic:"services",parent:"features",role:"persuader"},{topic:"features",parent:"features",role:"persuader"},{topic:"process",parent:"features",role:"persuader"},{topic:"menu",parent:"features",role:"persuader"},{topic:"inventory",parent:"features",role:"converter"},{topic:"integrations",parent:"features",role:"persuader"},{topic:"booking",parent:"cta",role:"converter"},{topic:"contact_form",parent:"cta",role:"converter"},{topic:"newsletter",parent:"cta",role:"converter"},{topic:"hours",parent:"cta",role:"persuader"},{topic:"location",parent:"cta",role:"persuader"},{topic:"cta",parent:"cta",role:"converter"},{topic:"blog",parent:"generic",role:"persuader"},{topic:"careers",parent:"generic",role:"persuader"},{topic:"legal",parent:"generic",role:"persuader"},{topic:"generic",parent:"generic",role:"persuader"}],b=new Map(u.map(e=>[e.topic,e]));function s(e){var a;return(a=b.get(e))!=null?a:{topic:"generic",parent:"generic",role:"persuader"}}var g=[["cookie_banner",/\bcookies?\b[^.]{0,20}\b(banner|notice|consent|policy)\b|consent banner/i],["breadcrumb",/\bbreadcrumbs?\b/i],["insurance",/\b(insurance|insurer|insured)\b/i],["warranty",/\b(warrant(y|ies)|guarantees?|guaranteed)\b/i],["certifications",/\b(certifi|accredit|licen[cs]ed)\b|approved by|iso ?\d/i],["security",/\b(security|privacy|compliance|gdpr|encrypt)\b|soc ?2/i],["about",/why choose|why us|about us|our story|who we are|years? (of )?(experience|serving)/i],["team",/our team|meet the team|our people|our staff/i],["reviews",/\b(reviews?|ratings?|testimonial)\b|what our customers say/i],["brands",/\b(brands?|partners?|manufacturers?|logos)\b|we work with|trusted by|loved by/i],["gallery",/\b(galler(y|ies)|photos?)\b|before and after/i],["case_study",/case stud|selected work|portfolio|our projects?|recent work/i],["awards",/\b(awards?|accolades?)\b/i],["pricing_plans",/\b(pricing|price list|per month|\/mo|subscriptions?)\b|\bplans?\b(?=[^.]{0,40}(from|start|month|year|[$€£]))/i],["quote",/get a quote|request a quote|free estimate|get an estimate/i],["financing",/\b(financ(e|ing)|instal?ments?)\b|payment plan|pay monthly/i],["booking",/\b(book|booking|reserve|reservation|appointments?)\b|schedule a/i],["newsletter",/\b(newsletter|subscribe)\b|join our (list|mailing)|mailing list/i],["contact_form",/contact us|get in touch|start a project|\benquir|\binquir|send us/i],["hours",/opening hours|\bhours\b|open (mon|tue|wed|thu|fri|sat|sun)/i],["location",/find us|our location|directions|where we are|visit us/i],["menu",/\b(menus?|dishes)\b|our food/i],["inventory",/\bbest ?sellers?\b|\b(shop|products?|collection)\b|our range|in stock/i],["process",/our process|how it works|the process|what to expect|\bsteps?\b/i],["services",/\b(services?|repairs?|treatments?|specialit|specialt)\b|what we (do|offer)/i],["integrations",/\bintegrations?\b|works with|connects? to/i],["features",/\b(features?|benefits?|capabilit)\b|what you get/i],["faq",/\bfaq\b|frequently asked|common questions?/i],["comparison",/\b(compare|comparison|versus)\b|\bvs\./i],["blog",/\b(blog|news|articles?|insights?)\b/i],["careers",/\b(careers?|jobs?)\b|join (the|our) team|we.re hiring/i],["legal",/\bterms\b|privacy policy|cookie policy|\blegal\b/i]],d={FAQPage:"faq",Question:"faq",Review:"reviews",AggregateRating:"reviews",Offer:"pricing_plans",AggregateOffer:"pricing_plans",PriceSpecification:"pricing_plans",Service:"services",Product:"inventory",ItemList:"inventory",Menu:"menu",MenuSection:"menu",LocalBusiness:"location",PostalAddress:"location",OpeningHoursSpecification:"hours",BreadcrumbList:"breadcrumb",ImageGallery:"gallery",JobPosting:"careers",BlogPosting:"blog",Article:"blog",Organization:"about"},f=[["pricing_plans",/(?:[$€£]\s?\d[\d,.]*\s*(?:\/|per\s)\s*(?:mo|month|yr|year|seat|user))|(?:\b(?:starter|basic|pro|growth|premium|enterprise)\b[^.]{0,60}[$€£]\s?\d)/i],["reviews",/(?:★{2,})|(?:\b\d(?:\.\d)?\s*(?:out of|\/)\s*5\b)|(?:\brated\b)|(?:["“][^"”]{20,160}["”]\s*[—–-]\s*[A-Z][a-z]+)/],["guarantee",/\b(?:money[- ]back guarantee|free returns?|returns? within|refund(?:s)? within|cancel anytime)\b/i],["security",/\b(?:encrypted|soc ?2|iso ?27001|gdpr[- ]compliant)\b/i],["comparison",/\b(?:vs|versus)\b\.?[^.!?]{0,80}\b(?:compare|comparison|plans?|features?|alternative)\b|\bhow (?:we|it) compares?\b/i]];function m(e){var n;let a=c(e);if(a)return o(t({},s(a)),{strength:"strong",stage:"structural"});for(let i of(n=e.structuredTypes)!=null?n:[]){let r=d[i];if(r)return o(t({},s(r)),{strength:"strong",stage:"structured_data"})}let l=`${e.idClass} ${e.headingText}`.toLowerCase();for(let[i,r]of g)if(r.test(l))return o(t({},s(i)),{strength:"strong",stage:"keyword"});for(let[i,r]of f)if(r.test(e.bodyText))return o(t({},s(i)),{strength:"strong",stage:"content"});return o(t({},s(p(e))),{strength:"weak",stage:"fallback"})}export{u as CLASSIFIER_TOPICS,m as classifyTopic};
2
+ //# sourceMappingURL=index-topics.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/engagement/topics.ts"],"sourcesContent":["// Rich topic vocabulary for section classification (Phase 2b, spec 2026-09-04).\n//\n// SERVER-ONLY. This module is deliberately NOT imported by ./classify.ts, which\n// the browser bundles: the tables below cost ~1.5 KB gzip, which is 7% of the\n// always-on snippet, on every page view of every customer site — for a layer\n// the browser never reads (classifySection returns only the parent).\n//\n// It shares structuralTopicOf/fallbackTopicOf with ./classify.ts, so client and\n// server agree on structure and fallbacks; only vocabulary richness differs.\n//\n// The topic→parent mapping is DUPLICATED from packages/policy/src/taxonomy.ts,\n// for the same bundle reason — core must not import the policy taxonomy.\n// apps/api/src/domain/classifier-taxonomy-sync.test.ts fails if the two drift.\n\nimport {\n structuralTopicOf,\n fallbackTopicOf,\n type SectionFeatures,\n type SemanticType,\n type TopicRule,\n} from './classify.js';\n\nexport const CLASSIFIER_TOPICS: readonly TopicRule[] = [\n { topic: 'navigation', parent: 'navigation', role: 'structural' },\n { topic: 'footer', parent: 'navigation', role: 'structural' },\n { topic: 'breadcrumb', parent: 'navigation', role: 'structural' },\n { topic: 'cookie_banner', parent: 'navigation', role: 'structural' },\n { topic: 'hero', parent: 'hero', role: 'converter' },\n { topic: 'pricing_plans', parent: 'pricing', role: 'converter' },\n { topic: 'quote', parent: 'pricing', role: 'converter' },\n { topic: 'financing', parent: 'pricing', role: 'persuader' },\n { topic: 'faq', parent: 'faq', role: 'persuader' },\n { topic: 'comparison', parent: 'comparison', role: 'persuader' },\n { topic: 'reviews', parent: 'social_proof', role: 'persuader' },\n { topic: 'testimonials', parent: 'social_proof', role: 'persuader' },\n { topic: 'case_study', parent: 'social_proof', role: 'persuader' },\n { topic: 'brands', parent: 'social_proof', role: 'persuader' },\n { topic: 'awards', parent: 'social_proof', role: 'persuader' },\n { topic: 'gallery', parent: 'social_proof', role: 'persuader' },\n { topic: 'stats', parent: 'social_proof', role: 'persuader' },\n { topic: 'insurance', parent: 'trust', role: 'persuader' },\n { topic: 'warranty', parent: 'trust', role: 'persuader' },\n { topic: 'certifications', parent: 'trust', role: 'persuader' },\n { topic: 'security', parent: 'trust', role: 'persuader' },\n { topic: 'guarantee', parent: 'trust', role: 'persuader' },\n { topic: 'about', parent: 'trust', role: 'persuader' },\n { topic: 'team', parent: 'trust', role: 'persuader' },\n { topic: 'services', parent: 'features', role: 'persuader' },\n { topic: 'features', parent: 'features', role: 'persuader' },\n { topic: 'process', parent: 'features', role: 'persuader' },\n { topic: 'menu', parent: 'features', role: 'persuader' },\n { topic: 'inventory', parent: 'features', role: 'converter' },\n { topic: 'integrations', parent: 'features', role: 'persuader' },\n { topic: 'booking', parent: 'cta', role: 'converter' },\n { topic: 'contact_form', parent: 'cta', role: 'converter' },\n { topic: 'newsletter', parent: 'cta', role: 'converter' },\n { topic: 'hours', parent: 'cta', role: 'persuader' },\n { topic: 'location', parent: 'cta', role: 'persuader' },\n { topic: 'cta', parent: 'cta', role: 'converter' },\n { topic: 'blog', parent: 'generic', role: 'persuader' },\n { topic: 'careers', parent: 'generic', role: 'persuader' },\n { topic: 'legal', parent: 'generic', role: 'persuader' },\n { topic: 'generic', parent: 'generic', role: 'persuader' },\n];\n\nconst RULE = new Map(CLASSIFIER_TOPICS.map((t) => [t.topic, t]));\n\nfunction ruleFor(topic: string): TopicRule {\n return RULE.get(topic) ?? { topic: 'generic', parent: 'generic', role: 'persuader' };\n}\n\n// ORDER IS LOAD-BEARING — first hit wins, so specific must precede general:\n// * `insurance` precedes `services`, or \"Insurance Approved Repairs\" becomes\n// a services section on the strength of \"repairs\".\n// * `brands` precedes `case_study`, or \"Brands We Work With\" matches \"work\".\n// * `pricing_plans` requires real pricing context — see fix 3 in ./classify.ts.\nconst TOPIC_KEYWORDS: Array<[string, RegExp]> = [\n ['cookie_banner', /\\bcookies?\\b[^.]{0,20}\\b(banner|notice|consent|policy)\\b|consent banner/i],\n ['breadcrumb', /\\bbreadcrumbs?\\b/i],\n ['insurance', /\\b(insurance|insurer|insured)\\b/i],\n ['warranty', /\\b(warrant(y|ies)|guarantees?|guaranteed)\\b/i],\n ['certifications', /\\b(certifi|accredit|licen[cs]ed)\\b|approved by|iso ?\\d/i],\n ['security', /\\b(security|privacy|compliance|gdpr|encrypt)\\b|soc ?2/i],\n ['about', /why choose|why us|about us|our story|who we are|years? (of )?(experience|serving)/i],\n ['team', /our team|meet the team|our people|our staff/i],\n ['reviews', /\\b(reviews?|ratings?|testimonial)\\b|what our customers say/i],\n ['brands', /\\b(brands?|partners?|manufacturers?|logos)\\b|we work with|trusted by|loved by/i],\n ['gallery', /\\b(galler(y|ies)|photos?)\\b|before and after/i],\n ['case_study', /case stud|selected work|portfolio|our projects?|recent work/i],\n ['awards', /\\b(awards?|accolades?)\\b/i],\n ['pricing_plans', /\\b(pricing|price list|per month|\\/mo|subscriptions?)\\b|\\bplans?\\b(?=[^.]{0,40}(from|start|month|year|[$€£]))/i],\n ['quote', /get a quote|request a quote|free estimate|get an estimate/i],\n ['financing', /\\b(financ(e|ing)|instal?ments?)\\b|payment plan|pay monthly/i],\n ['booking', /\\b(book|booking|reserve|reservation|appointments?)\\b|schedule a/i],\n ['newsletter', /\\b(newsletter|subscribe)\\b|join our (list|mailing)|mailing list/i],\n ['contact_form', /contact us|get in touch|start a project|\\benquir|\\binquir|send us/i],\n ['hours', /opening hours|\\bhours\\b|open (mon|tue|wed|thu|fri|sat|sun)/i],\n ['location', /find us|our location|directions|where we are|visit us/i],\n ['menu', /\\b(menus?|dishes)\\b|our food/i],\n ['inventory', /\\bbest ?sellers?\\b|\\b(shop|products?|collection)\\b|our range|in stock/i],\n ['process', /our process|how it works|the process|what to expect|\\bsteps?\\b/i],\n ['services', /\\b(services?|repairs?|treatments?|specialit|specialt)\\b|what we (do|offer)/i],\n ['integrations', /\\bintegrations?\\b|works with|connects? to/i],\n ['features', /\\b(features?|benefits?|capabilit)\\b|what you get/i],\n ['faq', /\\bfaq\\b|frequently asked|common questions?/i],\n ['comparison', /\\b(compare|comparison|versus)\\b|\\bvs\\./i],\n ['blog', /\\b(blog|news|articles?|insights?)\\b/i],\n ['careers', /\\b(careers?|jobs?)\\b|join (the|our) team|we.re hiring/i],\n ['legal', /\\bterms\\b|privacy policy|cookie policy|\\blegal\\b/i],\n];\n\n/**\n * schema.org `@type` → topic. The highest-precision signal available, and free:\n * the crawler already has the HTML, and a site that publishes structured data\n * has told us what a section is rather than leaving us to infer it from copy.\n *\n * Ranked BELOW structural rules deliberately — many sites emit an Organization\n * or WebSite blob site-wide in the header or footer, and letting that outrank a\n * `<nav>` would turn every such navigation bar into an `about` section.\n */\nconst SCHEMA_TOPIC: Record<string, string> = {\n FAQPage: 'faq',\n Question: 'faq',\n Review: 'reviews',\n AggregateRating: 'reviews',\n Offer: 'pricing_plans',\n AggregateOffer: 'pricing_plans',\n PriceSpecification: 'pricing_plans',\n Service: 'services',\n Product: 'inventory',\n ItemList: 'inventory',\n Menu: 'menu',\n MenuSection: 'menu',\n LocalBusiness: 'location',\n PostalAddress: 'location',\n OpeningHoursSpecification: 'hours',\n BreadcrumbList: 'breadcrumb',\n ImageGallery: 'gallery',\n JobPosting: 'careers',\n BlogPosting: 'blog',\n Article: 'blog',\n Organization: 'about',\n};\n\nconst TOPIC_CONTENT: Array<[string, RegExp]> = [\n ['pricing_plans', /(?:[$€£]\\s?\\d[\\d,.]*\\s*(?:\\/|per\\s)\\s*(?:mo|month|yr|year|seat|user))|(?:\\b(?:starter|basic|pro|growth|premium|enterprise)\\b[^.]{0,60}[$€£]\\s?\\d)/i],\n ['reviews', /(?:★{2,})|(?:\\b\\d(?:\\.\\d)?\\s*(?:out of|\\/)\\s*5\\b)|(?:\\brated\\b)|(?:[\"“][^\"”]{20,160}[\"”]\\s*[—–-]\\s*[A-Z][a-z]+)/],\n ['guarantee', /\\b(?:money[- ]back guarantee|free returns?|returns? within|refund(?:s)? within|cancel anytime)\\b/i],\n ['security', /\\b(?:encrypted|soc ?2|iso ?27001|gdpr[- ]compliant)\\b/i],\n ['comparison', /\\b(?:vs|versus)\\b\\.?[^.!?]{0,80}\\b(?:compare|comparison|plans?|features?|alternative)\\b|\\bhow (?:we|it) compares?\\b/i],\n];\n\n/** Which classifier stage produced the label. Persisted as provenance (the\n * `page_semantics.source` column distinguishes structured_data from\n * heuristic), so it must name the stage that actually MATCHED — a section\n * that merely CONTAINS a JSON-LD blob but was classified by its `<nav>` tag\n * is heuristic evidence, not the site telling us what it is. */\nexport type ClassifierStage = 'structural' | 'structured_data' | 'keyword' | 'content' | 'fallback';\n\n/**\n * Classify a section into a topic, its parent, and its functional role.\n * Structure and fallbacks are the shared implementation from ./classify.ts, so\n * the browser cannot disagree with the server about what a `<nav>` is.\n */\nexport function classifyTopic(\n f: SectionFeatures,\n): TopicRule & { strength: 'strong' | 'weak'; stage: ClassifierStage } {\n const structural = structuralTopicOf(f);\n if (structural) return { ...ruleFor(structural), strength: 'strong', stage: 'structural' };\n\n // Stage 0: the site told us what this is. Beats every inferred keyword.\n for (const t of f.structuredTypes ?? []) {\n const topic = SCHEMA_TOPIC[t];\n if (topic) return { ...ruleFor(topic), strength: 'strong', stage: 'structured_data' };\n }\n\n const hay = `${f.idClass} ${f.headingText}`.toLowerCase();\n for (const [topic, re] of TOPIC_KEYWORDS) {\n if (re.test(hay)) return { ...ruleFor(topic), strength: 'strong', stage: 'keyword' };\n }\n for (const [topic, re] of TOPIC_CONTENT) {\n if (re.test(f.bodyText)) return { ...ruleFor(topic), strength: 'strong', stage: 'content' };\n }\n return { ...ruleFor(fallbackTopicOf(f)), strength: 'weak', stage: 'fallback' };\n}\n\nexport type { SemanticType, SectionFeatures, TopicRule };\n"],"mappings":"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","names":["CLASSIFIER_TOPICS","RULE","t","ruleFor","topic","_a","TOPIC_KEYWORDS","SCHEMA_TOPIC","TOPIC_CONTENT","classifyTopic","f","structural","structuralTopicOf","__spreadProps","__spreadValues","hay","re","fallbackTopicOf"]}
package/dist/index.d.cts CHANGED
@@ -1,3 +1,3 @@
1
- export { A as AssignResult, a as Assignment, B as BLOCK_ALIGNS, c as BLOCK_EMPHASES, d as BLOCK_FITS, e as BLOCK_GAPS, f as BLOCK_GRID_COLUMNS, g as BLOCK_HEADING_LEVELS, h as BLOCK_JUSTIFIES, i as BLOCK_RATIOS, j as BLOCK_SIZES, k as BLOCK_TEXT_ALIGNS, l as BLOCK_TONES, m as BLOCK_WEIGHTS, n as BadgeBlock, o as BlockAlign, p as BlockEmphasis, q as BlockFit, r as BlockGap, s as BlockJustify, t as BlockNode, u as BlockRatio, v as BlockSize, w as BlockTextAlign, x as BlockTone, y as BlockWeight, z as ButtonBlock, C as ComponentGoalOptions, D as ComponentWeightEntry, E as CompoundLocator, F as DecideInput, G as DecideOutcome, H as DecisionSnapshot, J as EventType, K as GoalDefinition, L as GoalOptions, N as GraphConfig, O as GraphSnapshot, P as GridBlock, Q as HeadingBlock, R as ImageBlock, S as LEGACY_SESSION_COOKIE_NAME, T as LOCAL_MODE_BANNER, U as LinkBlock, V as MAX_BLOCK_ARMS, W as MAX_BLOCK_CHILDREN, X as MAX_BLOCK_DEPTH, Y as MAX_BLOCK_NODES, Z as MAX_BLOCK_TEXT_LEN, _ as MicroSignalEmitter, $ as MicroSignalType, a0 as PROD_KEYLESS_ERROR, a2 as QueueConfig, a3 as SNAPSHOT_STORAGE_KEY_PREFIX, a4 as SectionMapEntry, a5 as SentientClient, a6 as SentientConfig, a7 as SentientEvent, a8 as SessionConfig, a9 as SessionManager, aa as SitePalette, ab as SlotConfigEntry, ac as SlotOps, ad as SpacerBlock, ae as StackBlock, af as TextBlock, ag as WeightEntry, ah as _registerConsentUpgradeInit, ai as attachMicroSignalDetectors, aj as grantConsent, ak as init, al as isDoNotTrackEnabled, am as readSnapshot, an as renderPrePaintScript, ap as sessionCookieName, aq as writeSnapshot } from './index-CXxvWxCB.cjs';
2
- export { A as AGENT_INTENTS, a as AgentIntent, b as SlotDeclInput, c as agentIntent, d as agentUaList, e as armOfResult, f as baselineResultFor, g as baselineSlots, i as classifiedAgents, j as deriveSessionSegment, k as detectDeviceClass, l as detectTimeOfDay, m as detectTrafficSource, n as matchedAgentToken, r as referrerDomainFromReferer, t as toWireSlot, u as uaTokenMatch } from './session-meta-BVvq5RBB.cjs';
1
+ export { A as AssignResult, a as Assignment, B as BLOCK_ALIGNS, c as BLOCK_EMPHASES, d as BLOCK_FITS, e as BLOCK_GAPS, f as BLOCK_GRID_COLUMNS, g as BLOCK_HEADING_LEVELS, h as BLOCK_JUSTIFIES, i as BLOCK_RATIOS, j as BLOCK_SIZES, k as BLOCK_TEXT_ALIGNS, l as BLOCK_TONES, m as BLOCK_WEIGHTS, n as BadgeBlock, o as BlockAlign, p as BlockEmphasis, q as BlockFit, r as BlockGap, s as BlockJustify, t as BlockNode, u as BlockRatio, v as BlockSize, w as BlockTextAlign, x as BlockTone, y as BlockWeight, z as ButtonBlock, C as ComponentGoalOptions, D as ComponentWeightEntry, E as CompoundLocator, F as DecideInput, G as DecideOutcome, H as DecisionSnapshot, J as EventType, K as GoalDefinition, L as GoalOptions, N as GraphConfig, O as GraphSnapshot, P as GridBlock, Q as HeadingBlock, R as ImageBlock, S as LEGACY_SESSION_COOKIE_NAME, T as LOCAL_MODE_BANNER, U as LinkBlock, V as MAX_BLOCK_ARMS, W as MAX_BLOCK_CHILDREN, X as MAX_BLOCK_DEPTH, Y as MAX_BLOCK_NODES, Z as MAX_BLOCK_TEXT_LEN, _ as MicroSignalEmitter, $ as MicroSignalType, a0 as PROD_KEYLESS_ERROR, a2 as QueueConfig, a3 as SNAPSHOT_STORAGE_KEY_PREFIX, a4 as SectionMapEntry, a5 as SentientClient, a6 as SentientConfig, a7 as SentientEvent, a8 as SessionConfig, a9 as SessionManager, aa as SitePalette, ab as SlotConfigEntry, ac as SlotOps, ad as SpacerBlock, ae as StackBlock, af as TextBlock, ag as WeightEntry, ah as _registerConsentUpgradeInit, ai as attachMicroSignalDetectors, aj as grantConsent, ak as init, al as isDoNotTrackEnabled, am as readSnapshot, an as renderPrePaintScript, ap as sessionCookieName, aq as writeSnapshot } from './index-Cd0gMdJv.cjs';
2
+ export { A as AGENT_INTENTS, a as AgentIntent, C as CLICK_ID_KEYS, b as SlotDeclInput, c as agentIntent, d as agentUaList, e as armOfResult, f as baselineResultFor, g as baselineSlots, i as classifiedAgents, j as deriveSessionSegment, k as detectDeviceClass, l as detectTimeOfDay, m as detectTrafficSource, n as extractTrackedParams, o as matchedAgentToken, r as referrerDomainFromReferer, t as toWireSlot, u as uaTokenMatch } from './session-meta-C-bMqVeq.cjs';
3
3
  export { SlotResult } from '@sentientui/policy';
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { A as AssignResult, a as Assignment, B as BLOCK_ALIGNS, c as BLOCK_EMPHASES, d as BLOCK_FITS, e as BLOCK_GAPS, f as BLOCK_GRID_COLUMNS, g as BLOCK_HEADING_LEVELS, h as BLOCK_JUSTIFIES, i as BLOCK_RATIOS, j as BLOCK_SIZES, k as BLOCK_TEXT_ALIGNS, l as BLOCK_TONES, m as BLOCK_WEIGHTS, n as BadgeBlock, o as BlockAlign, p as BlockEmphasis, q as BlockFit, r as BlockGap, s as BlockJustify, t as BlockNode, u as BlockRatio, v as BlockSize, w as BlockTextAlign, x as BlockTone, y as BlockWeight, z as ButtonBlock, C as ComponentGoalOptions, D as ComponentWeightEntry, E as CompoundLocator, F as DecideInput, G as DecideOutcome, H as DecisionSnapshot, J as EventType, K as GoalDefinition, L as GoalOptions, N as GraphConfig, O as GraphSnapshot, P as GridBlock, Q as HeadingBlock, R as ImageBlock, S as LEGACY_SESSION_COOKIE_NAME, T as LOCAL_MODE_BANNER, U as LinkBlock, V as MAX_BLOCK_ARMS, W as MAX_BLOCK_CHILDREN, X as MAX_BLOCK_DEPTH, Y as MAX_BLOCK_NODES, Z as MAX_BLOCK_TEXT_LEN, _ as MicroSignalEmitter, $ as MicroSignalType, a0 as PROD_KEYLESS_ERROR, a2 as QueueConfig, a3 as SNAPSHOT_STORAGE_KEY_PREFIX, a4 as SectionMapEntry, a5 as SentientClient, a6 as SentientConfig, a7 as SentientEvent, a8 as SessionConfig, a9 as SessionManager, aa as SitePalette, ab as SlotConfigEntry, ac as SlotOps, ad as SpacerBlock, ae as StackBlock, af as TextBlock, ag as WeightEntry, ah as _registerConsentUpgradeInit, ai as attachMicroSignalDetectors, aj as grantConsent, ak as init, al as isDoNotTrackEnabled, am as readSnapshot, an as renderPrePaintScript, ap as sessionCookieName, aq as writeSnapshot } from './index-BbrtAtrY.js';
2
- export { A as AGENT_INTENTS, a as AgentIntent, b as SlotDeclInput, c as agentIntent, d as agentUaList, e as armOfResult, f as baselineResultFor, g as baselineSlots, i as classifiedAgents, j as deriveSessionSegment, k as detectDeviceClass, l as detectTimeOfDay, m as detectTrafficSource, n as matchedAgentToken, r as referrerDomainFromReferer, t as toWireSlot, u as uaTokenMatch } from './session-meta-BVvq5RBB.js';
1
+ export { A as AssignResult, a as Assignment, B as BLOCK_ALIGNS, c as BLOCK_EMPHASES, d as BLOCK_FITS, e as BLOCK_GAPS, f as BLOCK_GRID_COLUMNS, g as BLOCK_HEADING_LEVELS, h as BLOCK_JUSTIFIES, i as BLOCK_RATIOS, j as BLOCK_SIZES, k as BLOCK_TEXT_ALIGNS, l as BLOCK_TONES, m as BLOCK_WEIGHTS, n as BadgeBlock, o as BlockAlign, p as BlockEmphasis, q as BlockFit, r as BlockGap, s as BlockJustify, t as BlockNode, u as BlockRatio, v as BlockSize, w as BlockTextAlign, x as BlockTone, y as BlockWeight, z as ButtonBlock, C as ComponentGoalOptions, D as ComponentWeightEntry, E as CompoundLocator, F as DecideInput, G as DecideOutcome, H as DecisionSnapshot, J as EventType, K as GoalDefinition, L as GoalOptions, N as GraphConfig, O as GraphSnapshot, P as GridBlock, Q as HeadingBlock, R as ImageBlock, S as LEGACY_SESSION_COOKIE_NAME, T as LOCAL_MODE_BANNER, U as LinkBlock, V as MAX_BLOCK_ARMS, W as MAX_BLOCK_CHILDREN, X as MAX_BLOCK_DEPTH, Y as MAX_BLOCK_NODES, Z as MAX_BLOCK_TEXT_LEN, _ as MicroSignalEmitter, $ as MicroSignalType, a0 as PROD_KEYLESS_ERROR, a2 as QueueConfig, a3 as SNAPSHOT_STORAGE_KEY_PREFIX, a4 as SectionMapEntry, a5 as SentientClient, a6 as SentientConfig, a7 as SentientEvent, a8 as SessionConfig, a9 as SessionManager, aa as SitePalette, ab as SlotConfigEntry, ac as SlotOps, ad as SpacerBlock, ae as StackBlock, af as TextBlock, ag as WeightEntry, ah as _registerConsentUpgradeInit, ai as attachMicroSignalDetectors, aj as grantConsent, ak as init, al as isDoNotTrackEnabled, am as readSnapshot, an as renderPrePaintScript, ap as sessionCookieName, aq as writeSnapshot } from './index-BjVvN7cU.js';
2
+ export { A as AGENT_INTENTS, a as AgentIntent, C as CLICK_ID_KEYS, b as SlotDeclInput, c as agentIntent, d as agentUaList, e as armOfResult, f as baselineResultFor, g as baselineSlots, i as classifiedAgents, j as deriveSessionSegment, k as detectDeviceClass, l as detectTimeOfDay, m as detectTrafficSource, n as extractTrackedParams, o as matchedAgentToken, r as referrerDomainFromReferer, t as toWireSlot, u as uaTokenMatch } from './session-meta-C-bMqVeq.js';
3
3
  export { SlotResult } from '@sentientui/policy';