@sentientui/core 0.16.2 → 0.16.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-SHGSYP75.mjs +2 -0
- package/dist/{chunk-I2QGVQI6.mjs.map → chunk-SHGSYP75.mjs.map} +1 -1
- package/dist/{index-C247KMBw.d.ts → index-CTs3AM9E.d.ts} +3 -1
- package/dist/{index-CZLjrtM4.d.cts → index-DqbtJfra.d.cts} +3 -1
- package/dist/index-engagement.js +1 -1
- package/dist/index-engagement.js.map +1 -1
- package/dist/index-engagement.mjs +1 -1
- package/dist/index-graph.d.cts +2 -2
- package/dist/index-graph.d.ts +2 -2
- package/dist/index-graph.mjs +1 -1
- package/dist/index-local.d.cts +1 -1
- package/dist/index-local.d.ts +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
- package/dist/chunk-I2QGVQI6.mjs +0 -2
package/dist/index-engagement.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var w=Object.defineProperty;var q=Object.getOwnPropertyDescriptor;var H=Object.getOwnPropertyNames,G=Object.getOwnPropertySymbols;var W=Object.prototype.hasOwnProperty,V=Object.prototype.propertyIsEnumerable;var U=(t,e,n)=>e in t?w(t,e,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[e]=n,T=(t,e)=>{for(var n in e||(e={}))W.call(e,n)&&U(t,n,e[n]);if(G)for(var n of G(e))V.call(e,n)&&U(t,n,e[n]);return t};var Y=(t,e)=>{for(var n in e)w(t,n,{get:e[n],enumerable:!0})},J=(t,e,n,l)=>{if(e&&typeof e=="object"||typeof e=="function")for(let d of H(e))!W.call(t,d)&&d!==n&&w(t,d,{get:()=>e[d],enumerable:!(l=q(e,d))||l.enumerable});return t};var Q=t=>J(w({},"__esModule",{value:!0}),t);var oe={};Y(oe,{SEMANTIC_TYPES:()=>x,classifyFeatures:()=>k,classifySection:()=>C,featuresFromElement:()=>O,startEngagementCapture:()=>B});module.exports=Q(oe);var x=["pricing","hero","social_proof","cta","features","faq","comparison","trust","navigation","generic"],z=[["pricing",/\b(pricing|price|plans?|subscriptions?|per month|\/mo|tier)\b/i],["faq",/\b(faq|frequently asked|common questions?)\b/i],["comparison",/\b(compare|comparison|versus|vs\.)\b/i],["social_proof",/\b(testimonial|reviews?|trusted by|loved by|customers?|logos|rated|case stud)\b/i],["trust",/\b(security|guarantee|privacy|compliance|secure|gdpr|soc ?2|encrypt)\b/i],["features",/\b(features?|how it works|benefits?|capabilit|what you get)\b/i]],X=[["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],["social_proof",/(?:★{2,})|(?:\b\d(?:\.\d)?\s*(?:out of|\/)\s*5\b)|(?:\brated\b)|(?:["“][^"”]{20,160}["”]\s*[—–-]\s*[A-Z][a-z]+)/],["trust",/\b(?:money[- ]back guarantee|refund(?:s)? within|encrypted|soc ?2|iso ?27001|gdpr[- ]compliant|cancel anytime)\b/i],["comparison",/\b(?:vs|versus)\b\.?[^.!?]{0,80}\b(?:compare|comparison|plans?|features?|alternative)\b|\bhow (?:we|it) compares?\b/i]];function Z(t){var n;let e=t.querySelector("h1, h2, h3");return((n=e==null?void 0:e.textContent)!=null?n:"").slice(0,160)}function k(t){if(t.tag==="nav"||t.tag==="footer")return{type:"navigation",strength:"weak"};let e=`${t.idClass} ${t.headingText}`.toLowerCase();for(let[n,l]of z)if(l.test(e))return{type:n,strength:"strong"};for(let[n,l]of X)if(l.test(t.bodyText))return{type:n,strength:"strong"};return t.actionCount>=1&&t.textLength>0&&t.textLength<200?{type:"cta",strength:"weak"}:t.tag==="header"?{type:"hero",strength:"weak"}:/\b(hero|headline|banner)\b/i.test(e)?{type:"hero",strength:"weak"}:{type:"generic",strength:"weak"}}function O(t){var n,l;let e=((n=t.textContent)!=null?n:"").replace(/\s+/g," ").trim();return{tag:t.tagName.toLowerCase(),idClass:`${t.id} ${String((l=t.className)!=null?l:"")}`,headingText:Z(t),bodyText:e.slice(0,2e3),actionCount:t.querySelectorAll('a, button, [role="button"]').length,textLength:e.length}}function C(t){return k(O(t)).type}var ee=require("@sentientui/policy");function $(t,e,n,l){let d=[];{let i=!1,a=[],g=()=>{if(i)return;let f=Date.now();for(a.push(f);a.length>0&&f-a[0]>500;)a.shift();a.length>=3&&(i=!0,t("rage_click"))};e.addEventListener("click",g),d.push(()=>e.removeEventListener("click",g))}{let c=!1,p=i=>{if(c||!(i.target instanceof Node)||!e.contains(i.target)&&e!==i.target)return;c=!0;let a=typeof window!="undefined"?window.getSelection():null,g=a?a.toString().length:0;t("text_copy",{selectionLength:g})};document.addEventListener("copy",p),d.push(()=>document.removeEventListener("copy",p))}{let c=!1,p=!1,i=null,a=()=>{i!==null&&(clearTimeout(i),i=null)},g=()=>{c||!p||(a(),i=setTimeout(()=>{!c&&p&&(c=!0,t("scroll_hesitation"))},3e3))},f=()=>{a(),g()},S=b=>{for(let v of b)p=v.intersectionRatio>.3,p?g():a()},m=new IntersectionObserver(S,{threshold:[.3]});m.observe(e),window.addEventListener("scroll",f,{passive:!0}),d.push(()=>{m.disconnect(),window.removeEventListener("scroll",f),a()})}if((l==null?void 0:l.tabLoss)!==!1){let c=!1,p=n!=null?n:Date.now(),i=()=>{if(c||document.visibilityState!=="hidden")return;let a=Date.now()-p;a<15e3&&(c=!0,t("tab_loss",{timeOnPage:a}))};document.addEventListener("visibilitychange",i),d.push(()=>document.removeEventListener("visibilitychange",i))}return()=>{for(let c of d)c()}}function j(){return typeof navigator!="undefined"&&navigator.globalPrivacyControl===!0?!0:[typeof navigator!="undefined"?navigator.doNotTrack:void 0,typeof window!="undefined"?window.doNotTrack:void 0,typeof navigator!="undefined"?navigator.msDoNotTrack:void 0].some(e=>e==="1"||e==="yes")}var F="section, header, footer, nav, main > div, [data-sentient-section]";function te(t){var e;return((e=t.parentElement)==null?void 0:e.closest(F))!=null}function ne(t,e,n,l){try{fetch(`${e}/v1/section-map`,{method:"POST",keepalive:!0,headers:{"content-type":"application/json",authorization:`Bearer ${t}`},body:JSON.stringify({pageUrl:n,sections:l})}).catch(()=>{})}catch(d){}}var E=()=>{};function B(t,e){var R,D,_,A,P,N,L,M,K;let n=(R=e.doc)!=null?R:typeof document!="undefined"?document:void 0;if(!n||typeof IntersectionObserver=="undefined"||j()||!e.apiKey)return E;let l=(D=e.apiBase)!=null?D:"https://api.sentient-ui.com",d=Array.from(n.querySelectorAll(F)).filter(o=>!te(o));if(d.length===0)return E;let c=new Map,p=new Map,i=new Map;for(let o of d){let s=o.getAttribute("data-sentient-type"),r=s&&x.includes(s)?s:null,u=(A=r!=null?r:(_=e.typeOf)==null?void 0:_.call(e,o))!=null?A:C(o),h=`nc-${u}`;c.set(o,h),p.set(h,u),r?i.set(h,"markup"):i.has(h)||i.set(h,"auto")}let a=(M=(L=(N=(P=n.defaultView)!=null?P:typeof window!="undefined"?window:void 0)==null?void 0:N.location)==null?void 0:L.pathname)!=null?M:"/";ne(e.apiKey,l,a,[...p.entries()].map(([o,s])=>{var r;return{componentId:o,semanticType:s,source:(r=i.get(o))!=null?r:"auto"}}));let g=new Map,f=o=>{let s=g.get(o);return s||(s={ms:0,scroll:0,enterAt:null,intersecting:!1},g.set(o,s)),s},S=new IntersectionObserver(o=>{for(let s of o){let r=c.get(s.target);if(!r)continue;let u=f(r);s.isIntersecting?(u.intersecting=!0,u.enterAt=Date.now(),s.intersectionRatio>u.scroll&&(u.scroll=s.intersectionRatio)):(u.intersecting=!1,u.enterAt!=null&&(u.ms+=Date.now()-u.enterAt,u.enterAt=null))}},{threshold:[0,.25,.5,.75,1]});for(let o of c.keys())S.observe(o);let m=()=>{let o=Date.now();for(let[s,r]of g)if(r.enterAt!=null&&(r.ms+=o-r.enterAt,r.enterAt=null),!(r.ms<=0)){try{t.track({projectId:e.apiKey,componentId:s,eventType:"dwell",payload:{dwell_time:Math.round(r.ms),scroll_depth:Number(r.scroll.toFixed(2))}})}catch(u){}r.ms=0}},b=()=>{if(n.hidden)m();else{let o=Date.now();for(let s of g.values())s.intersecting&&(s.enterAt=o)}},v=()=>{m();try{S.disconnect()}catch(o){}};n.addEventListener("visibilitychange",b);let y=(K=n.defaultView)!=null?K:typeof window!="undefined"?window:void 0;y==null||y.addEventListener("pagehide",v);let I=[];if(e.microSignals)for(let[o,s]of c)I.push($((r,u={})=>{try{t.track({projectId:e.apiKey,componentId:s,eventType:"micro_signal",payload:T({signalType:r},u)})}catch(h){}},o));return()=>{m(),n.removeEventListener("visibilitychange",b),y==null||y.removeEventListener("pagehide",v);for(let o of I)o();try{S.disconnect()}catch(o){}}}0&&(module.exports={SEMANTIC_TYPES,classifyFeatures,classifySection,featuresFromElement,startEngagementCapture});
|
|
2
2
|
//# sourceMappingURL=index-engagement.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index-engagement.ts","../src/engagement/classify.ts","../src/index.ts","../src/micro-signals.ts","../src/engagement/capture.ts"],"sourcesContent":["// Lazy engagement entry — loaded on demand (mirrors ./graph) so the lean\n// bundle never carries the section classifier or IntersectionObserver logic.\nexport { startEngagementCapture, type EngagementCaptureOptions } from './engagement/capture';\nexport {\n classifyFeatures,\n classifySection,\n featuresFromElement,\n SEMANTIC_TYPES,\n type SectionFeatures,\n type SemanticType,\n} 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// The classifier is split into a pure feature-based core (classifyFeatures —\n// also used server-side on crawled HTML by the site-audit classification job)\n// and a DOM wrapper (classifySection). Content-based patterns detect\n// pricing/social-proof/trust/comparison from BODY TEXT, so div-soup pages with\n// uninformative ids/classes still classify (persona-coverage spec 2026-07-23).\n\nexport type SemanticType =\n | 'pricing' | 'hero' | 'social_proof' | 'cta' | 'features'\n | 'faq' | 'comparison' | 'trust' | 'navigation' | 'generic';\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\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};\n\n// Ordered most-specific first — the first keyword hit wins.\nconst KEYWORDS: Array<[SemanticType, RegExp]> = [\n ['pricing', /\\b(pricing|price|plans?|subscriptions?|per month|\\/mo|tier)\\b/i],\n ['faq', /\\b(faq|frequently asked|common questions?)\\b/i],\n ['comparison', /\\b(compare|comparison|versus|vs\\.)\\b/i],\n ['social_proof', /\\b(testimonial|reviews?|trusted by|loved by|customers?|logos|rated|case stud)\\b/i],\n ['trust', /\\b(security|guarantee|privacy|compliance|secure|gdpr|soc ?2|encrypt)\\b/i],\n ['features', /\\b(features?|how it works|benefits?|capabilit|what you get)\\b/i],\n];\n\n// Content-evidence patterns — run against bodyText. Ordered most-specific first.\n// Deliberately conservative: pricing needs per-period/plan context next to money\n// so an article mentioning \"$5 million\" stays generic.\nconst 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|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\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 or content\n * evidence (trustable enough to auto-apply); `weak` = structural fallback\n * (cta/hero/navigation/generic — capture-worthy but not persona evidence).\n */\nexport function classifyFeatures(f: SectionFeatures): { type: SemanticType; strength: 'strong' | 'weak' } {\n if (f.tag === 'nav' || f.tag === 'footer') return { type: 'navigation', strength: 'weak' };\n const hay = `${f.idClass} ${f.headingText}`.toLowerCase();\n for (const [type, re] of KEYWORDS) {\n if (re.test(hay)) return { type, strength: 'strong' };\n }\n for (const [type, re] of CONTENT_PATTERNS) {\n if (re.test(f.bodyText)) return { type, strength: 'strong' };\n }\n if (f.actionCount >= 1 && f.textLength > 0 && f.textLength < 200) return { type: 'cta', strength: 'weak' };\n if (f.tag === 'header') return { type: 'hero', strength: 'weak' };\n if (/\\b(hero|headline|banner)\\b/i.test(hay)) return { type: 'hero', strength: 'weak' };\n return { type: '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 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 };\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","import { initSession, type SessionConfig, type SessionManager } from './session';\nimport {\n createEventQueue,\n retryStorageKey,\n type EventQueue,\n type EventType,\n type QueueConfig,\n type SentientEvent,\n} from './queue';\nimport {\n createAssignmentCache,\n type Assignment,\n} from './cache';\nimport type {\n GraphConfig,\n GraphSnapshot,\n} from './graph';\nimport {\n detectDeviceClass,\n detectTrafficSource,\n detectTimeOfDay,\n referrerDomainFromReferer,\n uaTokenMatch,\n} from './session-meta.js';\nimport {\n toWireSlot,\n baselineResultFor,\n armOfResult,\n type SlotDeclInput,\n type SlotResult,\n} from './slots.js';\nimport { readSnapshot, writeSnapshot, SNAPSHOT_STORAGE_KEY_PREFIX, type SlotConfigEntry, type CompoundLocator } from './snapshot.js';\nimport { confidenceBand } from '@sentientui/policy';\nimport { createLocalModeClient } from './local-mode.js';\n\nexport { PROD_KEYLESS_ERROR, LOCAL_MODE_BANNER } from './local-mode.js';\n\nexport {\n detectDeviceClass,\n detectTrafficSource,\n detectTimeOfDay,\n deriveSessionSegment,\n referrerDomainFromReferer,\n uaTokenMatch,\n matchedAgentToken,\n agentUaList,\n} from './session-meta.js';\n\nconst DEFAULT_INGEST_URL = 'https://api.sentient-ui.com/v1/events';\n\n// Keyed by apiKey so multiple init() calls (HMR, multi-project) don't collide.\nconst _clients = new Map<string, {\n config: SentientConfig;\n upgrade: ((c: SentientClient) => void) | null;\n}>();\nlet _lastApiKey: string | null = null;\n\nexport type SentientConfig = {\n apiKey: string;\n context: 'landing' | 'ecommerce' | 'saas' | 'marketplace';\n /** @internal — not exposed to users; defaults to the hosted SentientUI API. */\n ingestUrl?: string;\n debug?: boolean;\n /**\n * Pre-seeded assignments from `preloadAssignments()` (SSR).\n * Seeds the local cache so `assign()` returns without a network call for\n * listed code variants, guaranteeing server and client render the same\n * variant on first paint. Managed-text components (assign with no\n * variantIds) still fetch once when the seed carries no content.\n */\n initialAssignments?: Record<string, string>;\n /**\n * Segment used for SSR preload (`device:source`). When set with `initialAssignments`,\n * seeds the assignment cache under this key so hydration matches the server bandit row.\n */\n sessionSegment?: string;\n /**\n * Consent gate. When `false`, returns a no-op client and performs no tracking.\n * Defaults to `true`. Re-call `init()` (via `AdaptiveProvider` consent prop) when\n * the user grants or revokes consent mid-session.\n */\n consent?: boolean;\n /**\n * Behavior before consent is granted. `'statistical_winner'` fetches the\n * best-performing variant via `GET /v1/winner` — no session or tracking data\n * is stored. `'control'` (default) shows `variantIds[0]` with no API call.\n * Applies when tracking is gated off — either `consent: false` or an active\n * Do Not Track signal.\n */\n preConsentBehavior?: 'statistical_winner' | 'control';\n /**\n * Whether to honor the browser's Do Not Track (DNT) signal. Defaults to `true`.\n * When `true` and the visitor has DNT enabled, the SDK sets no cookies and\n * sends no tracking data — behaving exactly as `consent: false` (still serving\n * the read-only `preConsentBehavior` winner if configured), and `grantConsent()`\n * will not upgrade it. Set `false` to make your own consent gate authoritative.\n */\n respectDoNotTrack?: boolean;\n userId?: string;\n /**\n * Session ID generated server-side (from `loadAdaptiveAssignments` / `loadAdaptiveDecision`).\n * When provided, the client adopts this ID on first visit instead of generating a new one,\n * ensuring events and goals are attributed to the same session the server used for assignment.\n */\n ssrSessionId?: string;\n /**\n * ISO 3166-1 alpha-2 country code for the visitor. When provided (e.g. from\n * the `CF-IPCountry` header in a Next.js server component), it is included in\n * the session upsert so country-based segmentation works without client-side\n * geo lookup.\n */\n country?: string;\n /**\n * Pre-seeded slot results from `preloadDecisions()` / `loadAdaptiveDecision()`\n * (SSR). Seeds the local slot state so `getSlotResult()` agrees with the\n * server-rendered markup on first paint.\n */\n initialSlots?: Record<string, SlotResult>;\n /**\n * Persona decided during SSR. Takes priority over the html-attribute\n * adoption and the local snapshot.\n */\n initialPersona?: { persona: string; confidence: number };\n /**\n * Keyless local mode. 'auto' (default) simulates decisions on-device when no\n * valid API key is configured — but only in development builds (the\n * `development` export condition); production bundles physically exclude the\n * engine. `true` forces the local engine regardless of key (escape hatch);\n * `false` restores the silent keyless no-op.\n */\n localMode?: 'auto' | boolean;\n};\n\nexport type AssignResult = { variantId: string; assignmentTtlMs: number; content?: string };\n\nexport type { SlotDeclInput, SlotResult };\nexport { armOfResult, baselineResultFor, baselineSlots, toWireSlot } from './slots.js';\n\nexport {\n SNAPSHOT_STORAGE_KEY_PREFIX,\n readSnapshot,\n writeSnapshot,\n renderPrePaintScript,\n} from './snapshot.js';\nexport type { DecisionSnapshot, SlotConfigEntry, SlotOps, CompoundLocator } from './snapshot.js';\n\n/** An editor-defined goal delivered with a registry-mode decision, for the\n * snippet to install delegated listeners from. */\nexport type GoalDefinition = {\n goalId: string;\n event: 'click' | 'form_submit' | 'url_reached';\n locator?: CompoundLocator;\n urlPattern?: string;\n slotId?: string;\n};\n\nexport type DecideOutcome = {\n layoutOrder: string[] | null;\n assignments: Record<string, string>;\n slots: Record<string, SlotResult>;\n persona: string;\n confidence: number;\n // Registry mode only: where/what to apply for server-defined slots.\n slotConfig?: Record<string, SlotConfigEntry>;\n // Registry mode only: editor-defined goals to wire up.\n goals?: GoalDefinition[];\n};\n\nexport type DecideInput = {\n sections?: string[];\n components?: Array<{ id: string; variantIds?: string[] }>;\n slots?: SlotDeclInput[];\n // 'registry' → serve the project's published slot_definitions in addition to\n // any declared slots (registry wins on id collision). Default 'request'.\n slotsFrom?: 'request' | 'registry';\n /**\n * Caller's build version (e.g. the snippet's `__SNIPPET_VERSION__`), sent\n * as `v` on the wire. Additive/best-effort: the server persists it for\n * version-skew reporting (see apps/api decide route) and ignores it\n * entirely on older deployments. Omit if the caller has no version to report.\n */\n v?: string;\n};\n\nexport type WeightEntry = { variantId: string; pulls: number; avgReward: number | null };\nexport type ComponentWeightEntry = { componentId: string; updatedAt: number; variants: WeightEntry[] };\n\nexport type ComponentGoalOptions = {\n /** Reward credited to the served variant (0–1). Defaults to 1. */\n reward?: number;\n /** Extra fields merged into the event payload. */\n metadata?: Record<string, unknown>;\n};\n\nexport type SentientClient = {\n track(\n event: Omit<SentientEvent, 'id' | 'sessionId' | 'timestamp' | 'timeInSession'>,\n ): void;\n goal(name: string, metadata?: Record<string, unknown>, weight?: number, stepIndex?: number): void;\n /**\n * Records a conversion attributed to the variant currently served for\n * `componentId`, so it feeds the per-variant CVR funnel. Resolves the served\n * variant from the local assignment cache — no need to pass variantId or\n * projectId. No-ops if the component has not been assigned yet (render its\n * `<Adaptive>`/call `assign()` first). Prefer this over bare `goal()` for\n * variant experiments; `goal()` is session-level only (no component attribution).\n */\n componentGoal(componentId: string, goalType: string, opts?: ComponentGoalOptions): void;\n identify(userId: string): void;\n getAssignment(componentId: string, segment: string): Assignment | null;\n /** Server-side variant assignment. Caches the result locally per (component, segment). */\n assign(componentId: string, variantIds?: string[], agentData?: unknown, agentDataByVariant?: Record<string, unknown>): Promise<AssignResult | null>;\n /**\n * Single-roundtrip decision for layout sections, component variants, and\n * adaptive slots. Awaits the session upsert (like `assign`) so the server\n * never decides for a session row that doesn't exist yet. A response\n * without a `slots` field means the server predates slots — every declared\n * slot resolves to its baseline and no retry is made.\n */\n decide(input: DecideInput): Promise<DecideOutcome | null>;\n /** Slot result served this session (decide result, SSR seed, snapshot, or failure baseline). Null when unknown. */\n getSlotResult(slotId: string): SlotResult | null;\n /** Current persona estimate. Band is always `confidenceBand(confidence)`. Null when nothing is known yet. */\n getPersona(): { persona: string; confidence: number; band: 'low' | 'medium' | 'high' } | null;\n /** Fetches current bandit weights for all components in this project. Used by the provider to keep live-weight polling fresh. */\n fetchWeights(): Promise<ComponentWeightEntry[]>;\n getGraph(): GraphSnapshot;\n /**\n * Routine teardown: stops timers/listeners and flushes pending events, but\n * KEEPS the visitor identity, decision snapshot, and retry bucket. Use for\n * component unmount / re-init (framework providers call this on cleanup).\n */\n dispose(): void;\n /**\n * Consent-revocation / forget-me teardown: everything `dispose()` does,\n * plus deletion of the visitor identity (`_snt_uid`), the decision\n * snapshot, and the persisted retry bucket. The next visit starts as a\n * brand-new visitor.\n */\n destroy(): void;\n /** True when this client is the keyless local-mode client (dev only). */\n readonly isLocal?: boolean;\n};\n\n// SSR preload helpers moved to the `@sentientui/core/server` entry in 0.6.0 so\n// ~200 lines of Node-only fetch logic stop shipping in the browser bundle.\n\nexport { attachMicroSignalDetectors } from './micro-signals.js';\nexport type { MicroSignalEmitter, MicroSignalType } from './micro-signals.js';\n\nexport type {\n SessionConfig,\n SessionManager,\n EventType,\n SentientEvent,\n QueueConfig,\n Assignment,\n GraphSnapshot,\n GraphConfig,\n};\n\nfunction generateEventId(): string {\n try {\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID();\n }\n } catch {\n /* fall through */\n }\n // Fallback: RFC 4122 v4 UUID built from Math.random() — avoids invalid UUID\n // syntax that would cause `invalid input syntax for type uuid` on INSERT.\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);\n });\n}\n\nconst SSR_CLIENT: SentientClient = {\n track: () => undefined,\n goal: () => undefined,\n componentGoal: () => undefined,\n identify: () => undefined,\n getAssignment: () => null,\n assign: () => Promise.resolve(null),\n decide: () => Promise.resolve(null),\n getSlotResult: () => null,\n getPersona: () => null,\n fetchWeights: () => Promise.resolve([]),\n getGraph: () => ({ pageNodes: [], capturedAt: 0 }),\n dispose: () => undefined,\n destroy: () => undefined,\n};\n\nfunction readUtmParams(): Record<string, string> {\n try {\n const out: Record<string, string> = {};\n const sp = new URLSearchParams(window.location.search);\n for (const [k, v] of sp) {\n if (k.startsWith('utm_')) out[k] = v;\n }\n return out;\n } catch {\n return {};\n }\n}\n\nfunction deriveBaseUrl(ingestUrl: string): string {\n return ingestUrl.replace(/\\/events\\/?$/, '');\n}\n\n/**\n * Detects whether the visitor has signalled a tracking opt-out. Honors Global\n * Privacy Control (`navigator.globalPrivacyControl`) — the legally-enforceable\n * CCPA/CPRA signal — as well as Do Not Track (`navigator.doNotTrack`, the legacy\n * `window.doNotTrack` on older Firefox, and `navigator.msDoNotTrack` on old\n * IE/Edge). GPC is a boolean; DNT is opt-out only when explicitly `'1'`/`'yes'`.\n */\nexport function isDoNotTrackEnabled(): boolean {\n // GPC is a boolean flag, checked separately from the DNT string signals.\n if (\n typeof navigator !== 'undefined' &&\n (navigator as unknown as { globalPrivacyControl?: boolean }).globalPrivacyControl === true\n ) {\n return true;\n }\n const signals = [\n typeof navigator !== 'undefined' ? navigator.doNotTrack : undefined,\n typeof window !== 'undefined'\n ? (window as unknown as { doNotTrack?: string | null }).doNotTrack\n : undefined,\n typeof navigator !== 'undefined'\n ? (navigator as unknown as { msDoNotTrack?: string | null }).msDoNotTrack\n : undefined,\n ];\n return signals.some((v) => v === '1' || v === 'yes');\n}\n\n/**\n * Upgrades a pre-consent client (created with `consent: false, preConsentBehavior: 'statistical_winner'`)\n * to a fully-tracking client. Call this from your consent management platform callback.\n * For React apps, prefer updating the `consent` prop on `<AdaptiveProvider>`.\n * Pass `apiKey` to target a specific project; omit to upgrade the most-recently-initialized client.\n */\nexport function grantConsent(apiKey?: string): void {\n if (typeof window === 'undefined') return;\n\n const key = apiKey ?? _lastApiKey;\n if (!key) {\n console.warn('[sentient] grantConsent() called before init()');\n return;\n }\n\n const entry = _clients.get(key);\n if (!entry) {\n console.warn('[sentient] grantConsent() called before init()');\n return;\n }\n\n const { config, upgrade } = entry;\n if (!upgrade) return;\n\n // Honor an active Do Not Track signal — consent cannot override a global opt-out.\n if (config.respectDoNotTrack !== false && isDoNotTrackEnabled()) return;\n\n const fullClient = init({ ...config, consent: true });\n upgrade(fullClient);\n _clients.set(key, { config: { ...config, consent: true }, upgrade: null });\n}\n\nfunction createPreConsentProxy(config: SentientConfig): { proxy: SentientClient; setInner: (c: SentientClient) => void } {\n const baseUrl = deriveBaseUrl(config.ingestUrl ?? DEFAULT_INGEST_URL);\n const authHeaders = {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${config.apiKey}`,\n } as const;\n\n let inner: SentientClient = {\n track: () => undefined,\n goal: () => undefined,\n componentGoal: () => undefined,\n identify: () => undefined,\n getAssignment: () => null,\n fetchWeights: () => Promise.resolve([]),\n async assign(componentId, variantIds, _agentData?) {\n try {\n const params = new URLSearchParams({ componentId });\n for (const v of variantIds ?? []) params.append('variantIds[]', v);\n const res = await fetch(`${baseUrl}/winner?${params.toString()}`, {\n headers: authHeaders,\n });\n if (!res.ok) return variantIds?.[0] ? { variantId: variantIds[0], assignmentTtlMs: 0 } : null;\n const body = (await res.json()) as { variantId: string };\n return { variantId: body.variantId, assignmentTtlMs: 0 };\n } catch {\n return variantIds?.[0] ? { variantId: variantIds[0], assignmentTtlMs: 0 } : null;\n }\n },\n decide: () => Promise.resolve(null),\n getSlotResult: () => null,\n getPersona: () => null,\n getGraph: () => ({ pageNodes: [], capturedAt: 0 }),\n dispose: () => undefined,\n destroy: () => undefined,\n };\n\n const proxy: SentientClient = {\n track: (e) => inner.track(e),\n goal: (n, m, w, s) => inner.goal(n, m, w, s),\n componentGoal: (c, g, o) => inner.componentGoal(c, g, o),\n identify: (u) => inner.identify(u),\n getAssignment: (c, s) => inner.getAssignment(c, s),\n assign: (c, v, a, av) => inner.assign(c, v, a, av),\n decide: (i) => inner.decide(i),\n getSlotResult: (s) => inner.getSlotResult(s),\n getPersona: () => inner.getPersona(),\n fetchWeights: () => inner.fetchWeights(),\n getGraph: () => inner.getGraph(),\n dispose: () => inner.dispose(),\n destroy: () => inner.destroy(),\n };\n\n function setInner(fullClient: SentientClient) {\n inner = fullClient;\n }\n\n return { proxy, setInner };\n}\n\n/**\n * Initializes the Sentient client. Returns a no-op client during SSR.\n */\nexport function init(config: SentientConfig): SentientClient {\n if (typeof window === 'undefined') {\n return SSR_CLIENT;\n }\n\n _lastApiKey = config.apiKey;\n\n // DNT/GPC (a global opt-out) or an explicit `consent: false` must be evaluated\n // BEFORE the local-mode branch: createLocalModeClient() calls initSession()\n // unconditionally, so a gated visitor would otherwise be issued the 365-day\n // `_snt_uid` identity cookie in keyless/local mode (audit P2). DNT/GPC also\n // gates tracking off even when the site passes `consent: true`, and blocks\n // `grantConsent()` from upgrading.\n const dntBlocked = config.respectDoNotTrack !== false && isDoNotTrackEnabled();\n const gated = config.consent === false || dntBlocked;\n\n // Keyless local mode. `localMode: true` forces the local engine (documented\n // escape hatch); 'auto' (default) engages it only when no valid key is\n // present. In production builds `@sentientui/core/local` resolves to a stub\n // and this degrades to a no-op client + one console.error per page load\n // (createLocalModeClient handles that), so no NODE_ENV check is needed here.\n const keyValid = typeof config.apiKey === 'string' && config.apiKey.startsWith('pk_');\n if (config.localMode === true || (!keyValid && config.localMode !== false)) {\n // A gated visitor must never get the identity cookie. Local mode has no\n // server to serve a statistical winner from, so return a plain no-op.\n if (gated) {\n _clients.set(config.apiKey || 'local', { config, upgrade: null });\n return SSR_CLIENT;\n }\n _clients.set(config.apiKey || 'local', { config, upgrade: null });\n return createLocalModeClient(config);\n }\n\n if (gated) {\n if (config.preConsentBehavior === 'statistical_winner') {\n if (!config.apiKey || !config.apiKey.startsWith('pk_')) {\n console.warn('[sentient] init() called with an invalid apiKey — expected a pk_ public key. SDK disabled.');\n return SSR_CLIENT;\n }\n const { proxy, setInner } = createPreConsentProxy(config);\n // Under DNT the read-only winner still serves, but consent can never\n // upgrade it to tracking — so drop the upgrade hook.\n _clients.set(config.apiKey, { config, upgrade: dntBlocked ? null : setInner });\n return proxy;\n }\n _clients.set(config.apiKey, { config, upgrade: null });\n return SSR_CLIENT;\n }\n\n if (!config.apiKey || !config.apiKey.startsWith('pk_')) {\n console.warn('[sentient] init() called with an invalid apiKey — expected a pk_ public key. SDK disabled.');\n return SSR_CLIENT;\n }\n\n if (config.ingestUrl === '') {\n console.warn('[sentient] init() called with an empty ingestUrl. SDK disabled.');\n return SSR_CLIENT;\n }\n\n const resolvedIngestUrl = config.ingestUrl ?? DEFAULT_INGEST_URL;\n\n const sessionStart = Date.now();\n const session = initSession({ ssrSessionId: config.ssrSessionId });\n const assignmentCache = createAssignmentCache();\n const eventQueue = createEventQueue({ ingestUrl: resolvedIngestUrl, apiKey: config.apiKey });\n const baseUrl = deriveBaseUrl(resolvedIngestUrl);\n\n const authHeaders = {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${config.apiKey}`,\n } as const;\n\n const deviceClass = detectDeviceClass(navigator.userAgent ?? '');\n const appOrigin = typeof window !== 'undefined' ? window.location.origin : undefined;\n const trafficSource = detectTrafficSource(document.referrer ?? '', appOrigin);\n const sessionSegment =\n config.sessionSegment ?? `${deviceClass}:${trafficSource}`;\n const inflightAssigns = new Map<string, Promise<AssignResult | null>>();\n\n // --- Adaptive-slot state (decide) ---\n // Results served for this session, keyed by slot id. Written by decide();\n // read by getSlotResult() (Task 3.3) and componentGoal's slot fallback.\n const slotStore = new Map<string, SlotResult>();\n let personaState: { persona: string; confidence: number } | null = null;\n\n // On decide failure every declared slot must still resolve — to its baseline.\n // Never overwrite a previously served result.\n const seedSlotBaselines = (decls: SlotDeclInput[]): void => {\n for (const d of decls) {\n if (!slotStore.has(d.id)) slotStore.set(d.id, baselineResultFor(d));\n }\n };\n\n // Seed slot/persona state. Priority: explicit SSR seeds → snapshot.\n if (config.initialSlots) {\n for (const [slotId, result] of Object.entries(config.initialSlots)) {\n slotStore.set(slotId, result);\n }\n }\n const seedSnapshot = readSnapshot(config.apiKey);\n if (seedSnapshot) {\n for (const [slotId, result] of Object.entries(seedSnapshot.slots)) {\n if (!slotStore.has(slotId)) slotStore.set(slotId, result);\n }\n }\n\n // Band-only persona sources (html attrs, snapshot) become a band-consistent\n // numeric confidence so confidenceBand(confidence) always equals the band.\n const BAND_CONFIDENCE: Record<string, number> = { low: 0.15, medium: 0.5, high: 0.85 };\n if (config.initialPersona) {\n personaState = { ...config.initialPersona };\n } else {\n // Single-writer rule: the inline pre-paint script owns the <html>\n // attributes. The client ADOPTS them as truth and never rewrites them\n // mid-session (next visit's script picks up the new snapshot instead).\n const ds = document.documentElement.dataset;\n if (ds.sentientPersona) {\n personaState = {\n persona: ds.sentientPersona,\n confidence: BAND_CONFIDENCE[ds.sentientConfidence ?? 'low'] ?? 0.15,\n };\n } else if (seedSnapshot) {\n personaState = {\n persona: seedSnapshot.persona,\n confidence: BAND_CONFIDENCE[seedSnapshot.band] ?? 0.15,\n };\n }\n }\n\n // Seed SSR-preloaded assignments into the local cache so assign() finds a\n // cache hit immediately — no network call, no variant flash on hydration.\n if (config.initialAssignments) {\n for (const [componentId, variantId] of Object.entries(config.initialAssignments)) {\n assignmentCache.set(componentId, sessionSegment, {\n variantId,\n assignedAt: Date.now(),\n segment: sessionSegment,\n confidence: 1,\n });\n }\n }\n\n // Upsert session metadata once on init. assign() awaits this promise so\n // the server isn't asked to assign for a session row that doesn't exist yet.\n let sessionReady: Promise<void> = Promise.resolve();\n\n const sessionId = session.getSessionId();\n if (sessionId) {\n const referrerDomain = referrerDomainFromReferer(document.referrer ?? '');\n const sessionBody = {\n sessionId,\n deviceClass,\n trafficSource,\n referrerDomain,\n utmParams: readUtmParams(),\n timeOfDay: detectTimeOfDay(new Date()),\n dayOfWeek: ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'][new Date().getDay()],\n ephemeral: session.isEphemeral(),\n // Likely-automation hint: navigator.webdriver (set under automation\n // control) or a known agent token in the UA. Probabilistic — used for\n // metrics + bandit exclusion server-side, never to change what's served.\n automation:\n (typeof navigator !== 'undefined' && navigator.webdriver === true) ||\n uaTokenMatch(navigator.userAgent ?? ''),\n ...(config.userId ? { userId: config.userId } : {}),\n ...(config.country ? { country: config.country } : {}),\n };\n try {\n sessionReady = fetch(`${baseUrl}/sessions`, {\n method: 'POST',\n keepalive: true,\n body: JSON.stringify(sessionBody),\n headers: authHeaders,\n })\n .then((res) => {\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 }\n return undefined;\n })\n .catch(() => undefined);\n } catch {\n /* never throw on init */\n }\n }\n\n if (config.debug) {\n console.log('[sentient] initialized', { context: config.context });\n (\n window as unknown as {\n __sentient?: {\n client: SentientClient;\n queue: EventQueue;\n };\n }\n ).__sentient = {\n client: null as unknown as SentientClient,\n queue: eventQueue,\n };\n }\n\n const client: SentientClient = {\n goal(name, metadata = {}, weight = 1.0, stepIndex = 0) {\n const sid = session.getSessionId();\n if (!sid) return;\n const goalId = generateEventId();\n sessionReady.then(() => {\n fetch(`${baseUrl}/goals`, {\n method: 'POST',\n keepalive: true,\n body: JSON.stringify({ sessionId: sid, name, metadata, weight, stepIndex, goalId }),\n headers: authHeaders,\n }).catch(() => undefined);\n });\n },\n\n componentGoal(componentId, goalType, opts) {\n const sid = session.getSessionId();\n if (!sid) return;\n // Variant experiments resolve from the assignment cache; adaptive slots\n // (useAdaptiveTokens / AdaptiveGroup) resolve from the slot state, using\n // the canonical arm string as the attributed variantId.\n const assignment = assignmentCache.get(componentId, sessionSegment);\n const slotResult = assignment ? null : slotStore.get(componentId) ?? null;\n if (!assignment && slotResult === null) {\n if (config.debug) {\n console.warn(\n `[sentient] componentGoal(\"${componentId}\"): no assignment or slot decision yet — render its <Adaptive>/adaptive hook or call assign()/decide() before recording a goal.`,\n );\n }\n return;\n }\n const attributedVariantId = assignment ? assignment.variantId : armOfResult(slotResult!);\n const fullEvent: SentientEvent = {\n id: generateEventId(),\n sessionId: sid,\n projectId: config.apiKey,\n componentId,\n variantId: attributedVariantId,\n eventType: 'goal_achieved',\n goalType,\n payload: { reward: opts?.reward ?? 1, ...(opts?.metadata ?? {}) },\n timestamp: Date.now(),\n timeInSession: Date.now() - sessionStart,\n };\n if (config.debug) {\n console.log('[sentient] componentGoal', fullEvent);\n }\n sessionReady.then(() => eventQueue.push(fullEvent));\n },\n\n identify(userId) {\n const sid = session.getSessionId();\n if (!sid) return;\n sessionReady.then(() => {\n fetch(`${baseUrl}/sessions`, {\n method: 'POST',\n keepalive: true,\n body: JSON.stringify({ sessionId: sid, userId, ephemeral: session.isEphemeral() }),\n headers: authHeaders,\n }).catch(() => undefined);\n });\n },\n\n track(event) {\n const sessionId = session.getSessionId();\n if (!sessionId) return;\n\n const fullEvent: SentientEvent = {\n ...event,\n id: generateEventId(),\n sessionId,\n timestamp: Date.now(),\n timeInSession: Date.now() - sessionStart,\n };\n\n if (config.debug) {\n console.log('[sentient] track', fullEvent);\n }\n\n sessionReady.then(() => eventQueue.push(fullEvent));\n },\n\n getAssignment(componentId, segment) {\n return assignmentCache.get(componentId, segment);\n },\n\n async assign(componentId, variantIds, agentData?, agentDataByVariant?) {\n const sid = session.getSessionId();\n if (!sid) return null;\n\n const cached = assignmentCache.get(componentId, sessionSegment);\n // When variantIds are provided (A/B code variant), a cache hit is always final.\n // When variantIds are absent (managed text component), only hit the cache if content\n // is present — a seed from initialAssignments has no content and must still fetch.\n if (cached && (variantIds?.length || cached.content !== undefined)) {\n return { variantId: cached.variantId, assignmentTtlMs: 0, content: cached.content };\n }\n\n // Coalesce concurrent assigns for the same component (e.g. several\n // mounted slots sharing one id) into a single network request.\n const inflight = inflightAssigns.get(componentId);\n if (inflight) return inflight;\n\n const request = (async (): Promise<AssignResult | null> => {\n await sessionReady;\n try {\n const body: Record<string, unknown> = { sessionId: sid, componentId, variantIds };\n if (agentDataByVariant !== undefined) body.agentDataByVariant = agentDataByVariant;\n else if (agentData !== undefined) body.agentData = agentData;\n const res = await fetch(`${baseUrl}/assign`, {\n method: 'POST',\n body: JSON.stringify(body),\n headers: authHeaders,\n });\n if (!res.ok) return null;\n const result = (await res.json()) as AssignResult;\n assignmentCache.set(componentId, sessionSegment, {\n variantId: result.variantId,\n assignedAt: Date.now(),\n segment: sessionSegment,\n confidence: 1,\n content: result.content,\n });\n return result;\n } catch {\n return null;\n } finally {\n inflightAssigns.delete(componentId);\n }\n })();\n inflightAssigns.set(componentId, request);\n return request;\n },\n\n async decide(input) {\n const sid = session.getSessionId();\n if (!sid) return null;\n const declared = input.slots ?? [];\n await sessionReady;\n try {\n const body: Record<string, unknown> = { sessionId: sid };\n if (input.sections && input.sections.length > 0) {\n body.sections = input.sections.map((id) => ({ id }));\n }\n body.components = input.components ?? [];\n if (declared.length > 0) body.slots = declared.map(toWireSlot);\n if (input.slotsFrom === 'registry') body.slotsFrom = 'registry';\n if (input.v) body.v = input.v;\n\n const res = await fetch(`${baseUrl}/decide`, {\n method: 'POST',\n body: JSON.stringify(body),\n headers: authHeaders,\n });\n if (!res.ok) {\n seedSlotBaselines(declared);\n return null;\n }\n const data = (await res.json()) as {\n layoutOrder?: string[] | null;\n assignments?: Record<string, string>;\n slots?: Record<string, SlotResult>;\n slotConfig?: Record<string, SlotConfigEntry>;\n goals?: GoalDefinition[];\n persona?: string;\n confidence?: number;\n };\n\n const slots: Record<string, SlotResult> = {};\n for (const d of declared) {\n // `data.slots === undefined` means the server predates the slots\n // contract: serve the declared baseline everywhere, do NOT retry.\n // (Distinct from `slots: {}`, which also falls back per-slot.)\n slots[d.id] = data.slots?.[d.id] ?? baselineResultFor(d);\n }\n // Registry mode: the server returns slots the request never declared.\n // Take them verbatim (classic mode returns only declared slots, so this\n // union is a no-op there — back-compatible).\n if (data.slots) {\n for (const [slotId, result] of Object.entries(data.slots)) {\n if (!(slotId in slots)) slots[slotId] = result;\n }\n }\n for (const [slotId, result] of Object.entries(slots)) slotStore.set(slotId, result);\n personaState = { persona: data.persona ?? 'unknown', confidence: data.confidence ?? 0 };\n\n // Seed component assignments so <Adaptive>/assign() agree with this\n // decide (same shape as the initialAssignments seed above).\n for (const [componentId, variantId] of Object.entries(data.assignments ?? {})) {\n assignmentCache.set(componentId, sessionSegment, {\n variantId,\n assignedAt: Date.now(),\n segment: sessionSegment,\n confidence: 1,\n });\n }\n\n // Persist for the next visit's pre-paint (SPA cache-first pattern).\n writeSnapshot(config.apiKey, {\n v: 1,\n persona: personaState.persona,\n band: confidenceBand(personaState.confidence),\n slots: Object.fromEntries(slotStore),\n layoutOrder: data.layoutOrder ?? null,\n savedAt: Date.now(),\n ...(data.slotConfig ? { slotConfig: data.slotConfig } : {}),\n });\n\n return {\n layoutOrder: data.layoutOrder ?? null,\n assignments: data.assignments ?? {},\n slots,\n persona: personaState.persona,\n confidence: personaState.confidence,\n ...(data.slotConfig ? { slotConfig: data.slotConfig } : {}),\n ...(data.goals ? { goals: data.goals } : {}),\n };\n } catch {\n seedSlotBaselines(declared);\n return null;\n }\n },\n\n getSlotResult(slotId) {\n return slotStore.get(slotId) ?? null;\n },\n\n getPersona() {\n if (!personaState) return null;\n return {\n persona: personaState.persona,\n confidence: personaState.confidence,\n band: confidenceBand(personaState.confidence),\n };\n },\n\n async fetchWeights() {\n try {\n const res = await fetch(`${baseUrl}/weights`, { headers: authHeaders });\n if (!res.ok) return [];\n const data = (await res.json()) as { components: ComponentWeightEntry[] };\n return data.components ?? [];\n } catch {\n return [];\n }\n },\n\n getGraph() {\n return { pageNodes: [], capturedAt: 0 };\n },\n\n dispose() {\n // Stops the flush timer and unload listeners (with a final flush) but\n // leaves identity, snapshot, and retry bucket for the next client.\n eventQueue.destroy();\n if (config.debug) {\n console.log('[sentient] disposed');\n }\n },\n\n destroy() {\n eventQueue.destroy();\n session.destroy();\n // Forget-me must be total: a surviving decision snapshot would\n // re-personalize the next visit via the pre-paint script, and a\n // persisted retry bucket would re-send events for the deleted identity.\n try {\n localStorage.removeItem(SNAPSHOT_STORAGE_KEY_PREFIX + config.apiKey);\n localStorage.removeItem(retryStorageKey(config.apiKey));\n } catch {\n /* storage unavailable — nothing persisted to remove */\n }\n if (config.debug) {\n console.log('[sentient] destroyed');\n }\n },\n };\n\n _clients.set(config.apiKey, { config, upgrade: null });\n\n if (config.debug) {\n const win = window as unknown as { __sentient?: { client: SentientClient } };\n if (win.__sentient) {\n win.__sentient.client = client;\n }\n }\n\n return client;\n}\n","export type MicroSignalEmitter = (\n signalType: 'rage_click' | 'text_copy' | 'scroll_hesitation' | 'tab_loss',\n extra?: Record<string, unknown>,\n) => void;\n\nexport type MicroSignalType = Parameters<MicroSignalEmitter>[0];\n\n/**\n * Attaches passive behavioral detectors to `node`. Calls `emit` when a signal\n * fires. Each signal type fires at most once per call to this function.\n * Returns a cleanup function that removes all listeners.\n */\nexport function attachMicroSignalDetectors(\n emit: MicroSignalEmitter,\n node: Element,\n variantAssignedAt?: number,\n): () => void {\n const cleanups: Array<() => void> = [];\n\n // --- Rage click: 3+ clicks within 500ms ---\n {\n const WINDOW_MS = 500;\n const THRESHOLD = 3;\n let firedOnce = false;\n const timestamps: number[] = [];\n\n const onClick = (): void => {\n if (firedOnce) return;\n const now = Date.now();\n timestamps.push(now);\n while (timestamps.length > 0 && now - timestamps[0]! > WINDOW_MS) {\n timestamps.shift();\n }\n if (timestamps.length >= THRESHOLD) {\n firedOnce = true;\n emit('rage_click');\n }\n };\n\n node.addEventListener('click', onClick);\n cleanups.push(() => node.removeEventListener('click', onClick));\n }\n\n // --- Text copy: copy event within node ---\n {\n let firedOnce = false;\n\n const onCopy = (e: Event): void => {\n if (firedOnce) return;\n if (!(e.target instanceof Node)) return;\n if (!node.contains(e.target) && node !== e.target) return;\n firedOnce = true;\n const sel = typeof window !== 'undefined' ? window.getSelection() : null;\n const selectionLength = sel ? sel.toString().length : 0;\n emit('text_copy', { selectionLength });\n };\n\n document.addEventListener('copy', onCopy);\n cleanups.push(() => document.removeEventListener('copy', onCopy));\n }\n\n // --- Scroll hesitation: scroll stops 3s while component visible ---\n {\n let firedOnce = false;\n let isVisible = false;\n let hesitationTimer: ReturnType<typeof setTimeout> | null = null;\n\n const clearHesitation = (): void => {\n if (hesitationTimer !== null) {\n clearTimeout(hesitationTimer);\n hesitationTimer = null;\n }\n };\n\n const startHesitation = (): void => {\n if (firedOnce || !isVisible) return;\n clearHesitation();\n hesitationTimer = setTimeout(() => {\n if (!firedOnce && isVisible) {\n firedOnce = true;\n emit('scroll_hesitation');\n }\n }, 3000);\n };\n\n const onScroll = (): void => {\n clearHesitation();\n startHesitation();\n };\n\n const ioCallback = (entries: IntersectionObserverEntry[]): void => {\n for (const entry of entries) {\n isVisible = entry.intersectionRatio > 0.3;\n if (!isVisible) clearHesitation();\n else startHesitation();\n }\n };\n const io = new IntersectionObserver(ioCallback, { threshold: [0.3] });\n io.observe(node);\n window.addEventListener('scroll', onScroll, { passive: true });\n\n cleanups.push(() => {\n io.disconnect();\n window.removeEventListener('scroll', onScroll);\n clearHesitation();\n });\n }\n\n // --- Tab loss: tab hidden within 15s of variant_assigned ---\n {\n let firedOnce = false;\n const assignedAt = variantAssignedAt ?? Date.now();\n\n const onVisibility = (): void => {\n if (firedOnce) return;\n if (document.visibilityState !== 'hidden') return;\n const elapsed = Date.now() - assignedAt;\n if (elapsed < 15_000) {\n firedOnce = true;\n emit('tab_loss', { timeOnPage: elapsed });\n }\n };\n\n document.addEventListener('visibilitychange', onVisibility);\n cleanups.push(() => document.removeEventListener('visibilitychange', onVisibility));\n }\n\n return () => {\n for (const c of cleanups) c();\n };\n}\n","import { classifySection, SEMANTIC_TYPES, type SemanticType } from './classify';\nimport { isDoNotTrackEnabled } from '../index.js';\nimport { attachMicroSignalDetectors } from '../micro-signals.js';\n\n// Shared engagement capture (spec 2026-07-22-persona-signal-capture). Detects\n// semantic sections, registers them via /v1/section-map, and records per-section\n// dwell/scroll via IntersectionObserver — emitting the same 'dwell' events the\n// persona pipeline consumes. Used by the React provider (default on) and the\n// no-code snippet. Defense-in-depth: checks DNT internally even though callers\n// gate on consent/DNT too; a missing IntersectionObserver → no-op.\n\ntype CaptureClient = {\n track(event: { projectId: string; componentId: string; eventType: string; payload: Record<string, unknown> }): void;\n};\n\nexport type EngagementCaptureOptions = {\n apiKey: string;\n /** API base, no trailing slash. Defaults to the hosted API. */\n apiBase?: string;\n doc?: Document;\n /**\n * Also attach per-section micro-signal detectors (rage click, text copy,\n * scroll hesitation, tab loss), attributed to the section's `nc-<type>`\n * component. For the no-code snippet, whose pages have no `<Adaptive>`\n * components carrying their own detectors. Default false — the React SDK\n * keeps its per-component detectors and must not double-attach.\n */\n microSignals?: boolean;\n /**\n * Server-served section-map lookup (persona-coverage auto-classification):\n * consulted after explicit `data-sentient-type` markup, before the local\n * heuristic. Return null when the element has no served label.\n */\n typeOf?: (el: Element) => SemanticType | null;\n};\n\nconst SECTION_SELECTOR = 'section, header, footer, nav, main > div, [data-sentient-section]';\n\n/** Skip a section nested inside another candidate section (avoid double count). */\nfunction isNested(el: Element): boolean {\n return el.parentElement?.closest(SECTION_SELECTOR) != null;\n}\n\nfunction registerSections(\n apiKey: string,\n apiBase: string,\n pageUrl: string,\n sections: Array<{ componentId: string; semanticType: SemanticType; source: 'markup' | 'auto' }>,\n): void {\n try {\n void fetch(`${apiBase}/v1/section-map`, {\n method: 'POST',\n keepalive: true,\n headers: { 'content-type': 'application/json', authorization: `Bearer ${apiKey}` },\n body: JSON.stringify({ pageUrl, sections }),\n }).catch(() => undefined);\n } catch {\n /* fail-safe */\n }\n}\n\nconst NOOP = (): void => undefined;\n\nexport function startEngagementCapture(\n client: CaptureClient,\n opts: EngagementCaptureOptions,\n): () => void {\n const doc = opts.doc ?? (typeof document !== 'undefined' ? document : undefined);\n if (!doc || typeof IntersectionObserver === 'undefined') return NOOP;\n if (isDoNotTrackEnabled()) return NOOP;\n // Keyless zero-network contract: capture exists to feed the hosted persona\n // pipeline — with no api key there is nothing to feed, and the section-map\n // registration fetch must never fire.\n if (!opts.apiKey) return NOOP;\n const apiBase = opts.apiBase ?? 'https://api.sentient-ui.com';\n\n const els = Array.from(doc.querySelectorAll(SECTION_SELECTOR)).filter((el) => !isNested(el));\n if (els.length === 0) return NOOP;\n\n // Collapse to one component per semantic type per page (the matrix aggregates\n // by semantic type anyway). Per-element precedence: explicit data-sentient-type\n // markup → served section map (opts.typeOf) → local heuristic.\n const componentOf = new Map<Element, string>();\n const types = new Map<string, SemanticType>();\n const sources = new Map<string, 'markup' | 'auto'>();\n for (const el of els) {\n const explicit = el.getAttribute('data-sentient-type');\n const markup = explicit && (SEMANTIC_TYPES as readonly string[]).includes(explicit)\n ? (explicit as SemanticType)\n : null;\n const type = markup ?? opts.typeOf?.(el) ?? classifySection(el);\n const componentId = `nc-${type}`;\n componentOf.set(el, componentId);\n types.set(componentId, type);\n // Markup wins if the same collapsed component gets both provenances.\n if (markup) sources.set(componentId, 'markup');\n else if (!sources.has(componentId)) sources.set(componentId, 'auto');\n }\n\n const pageUrl = (doc.defaultView ?? (typeof window !== 'undefined' ? window : undefined))?.location?.pathname ?? '/';\n registerSections(opts.apiKey, apiBase, pageUrl, [...types.entries()].map(([componentId, semanticType]) => ({\n componentId, semanticType, source: sources.get(componentId) ?? 'auto',\n })));\n\n // Accumulate visible dwell (ms) + max scroll ratio per component. `intersecting`\n // tracks in-viewport state independently of `enterAt` (the running clock) so a\n // tab-hide can pause the clock and a tab-show can resume it for still-visible\n // sections — IntersectionObserver does not re-fire on visibilitychange.\n const state = new Map<string, { ms: number; scroll: number; enterAt: number | null; intersecting: boolean }>();\n const get = (id: string) => {\n let s = state.get(id);\n if (!s) { s = { ms: 0, scroll: 0, enterAt: null, intersecting: false }; state.set(id, s); }\n return s;\n };\n\n const observer = new IntersectionObserver((entries) => {\n for (const entry of entries) {\n const id = componentOf.get(entry.target);\n if (!id) continue;\n const s = get(id);\n if (entry.isIntersecting) {\n s.intersecting = true;\n s.enterAt = Date.now();\n if (entry.intersectionRatio > s.scroll) s.scroll = entry.intersectionRatio;\n } else {\n s.intersecting = false;\n if (s.enterAt != null) { s.ms += Date.now() - s.enterAt; s.enterAt = null; }\n }\n }\n }, { threshold: [0, 0.25, 0.5, 0.75, 1] });\n for (const el of componentOf.keys()) observer.observe(el);\n\n // Bank accumulated dwell and RESET the accumulators (so a later emit can't\n // double-count) WITHOUT disconnecting — a visitor who hides/re-shows the tab\n // or tab-switches keeps being measured. Pauses the running clock; the tab-show\n // handler restarts it for still-visible sections so hidden time isn't counted.\n const emit = (): void => {\n const now = Date.now();\n for (const [id, s] of state) {\n if (s.enterAt != null) { s.ms += now - s.enterAt; s.enterAt = null; }\n if (s.ms <= 0) continue;\n try {\n client.track({\n projectId: opts.apiKey, // SDK convention: server derives the real project from the key\n componentId: id,\n eventType: 'dwell',\n payload: { dwell_time: Math.round(s.ms), scroll_depth: Number(s.scroll.toFixed(2)) },\n });\n } catch {\n /* fail-safe */\n }\n s.ms = 0;\n }\n };\n\n const onVisibility = (): void => {\n if (doc.hidden) {\n emit(); // bank + pause\n } else {\n const now = Date.now(); // resume the clock for sections still on screen\n for (const s of state.values()) if (s.intersecting) s.enterAt = now;\n }\n };\n const onPageHide = (): void => {\n emit();\n try { observer.disconnect(); } catch { /* ignore */ }\n };\n doc.addEventListener('visibilitychange', onVisibility);\n const win = doc.defaultView ?? (typeof window !== 'undefined' ? window : undefined);\n win?.addEventListener('pagehide', onPageHide);\n\n // Per-section micro-signal detectors (opt-in; see EngagementCaptureOptions).\n // Attributed to the section's nc-<type> id with no variant — they feed the\n // persona attention fallback and auto-discovery, never rewards.\n const detectorCleanups: Array<() => void> = [];\n if (opts.microSignals) {\n for (const [el, componentId] of componentOf) {\n detectorCleanups.push(\n attachMicroSignalDetectors((signalType, extra = {}) => {\n try {\n client.track({\n projectId: opts.apiKey,\n componentId,\n eventType: 'micro_signal',\n payload: { signalType, ...extra },\n });\n } catch {\n /* fail-safe */\n }\n }, el),\n );\n }\n }\n\n // Cleanup: bank any remaining dwell, then detach everything (provider unmount\n // / consent re-init must not leak observers or listeners).\n return () => {\n emit();\n doc.removeEventListener('visibilitychange', onVisibility);\n win?.removeEventListener('pagehide', onPageHide);\n for (const c of detectorCleanups) c();\n try { observer.disconnect(); } catch { /* ignore */ }\n };\n}\n"],"mappings":"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","names":["index_engagement_exports","__export","SEMANTIC_TYPES","classifyFeatures","classifySection","featuresFromElement","startEngagementCapture","__toCommonJS","SEMANTIC_TYPES","KEYWORDS","CONTENT_PATTERNS","headingText","el","_a","h","classifyFeatures","f","hay","type","re","featuresFromElement","_b","text","classifySection","import_policy","attachMicroSignalDetectors","emit","node","variantAssignedAt","cleanups","firedOnce","timestamps","onClick","now","onCopy","e","sel","selectionLength","isVisible","hesitationTimer","clearHesitation","startHesitation","onScroll","ioCallback","entries","entry","io","assignedAt","onVisibility","elapsed","c","isDoNotTrackEnabled","v","SECTION_SELECTOR","isNested","el","_a","registerSections","apiKey","apiBase","pageUrl","sections","e","NOOP","startEngagementCapture","client","opts","_b","_c","_d","_e","_f","_g","_h","_i","doc","isDoNotTrackEnabled","els","componentOf","types","sources","explicit","markup","SEMANTIC_TYPES","type","classifySection","componentId","semanticType","state","get","id","s","observer","entries","entry","emit","now","onVisibility","onPageHide","win","detectorCleanups","attachMicroSignalDetectors","signalType","extra","__spreadValues","c"]}
|
|
1
|
+
{"version":3,"sources":["../src/index-engagement.ts","../src/engagement/classify.ts","../src/index.ts","../src/micro-signals.ts","../src/engagement/capture.ts"],"sourcesContent":["// Lazy engagement entry — loaded on demand (mirrors ./graph) so the lean\n// bundle never carries the section classifier or IntersectionObserver logic.\nexport { startEngagementCapture, type EngagementCaptureOptions } from './engagement/capture';\nexport {\n classifyFeatures,\n classifySection,\n featuresFromElement,\n SEMANTIC_TYPES,\n type SectionFeatures,\n type SemanticType,\n} 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// The classifier is split into a pure feature-based core (classifyFeatures —\n// also used server-side on crawled HTML by the site-audit classification job)\n// and a DOM wrapper (classifySection). Content-based patterns detect\n// pricing/social-proof/trust/comparison from BODY TEXT, so div-soup pages with\n// uninformative ids/classes still classify (persona-coverage spec 2026-07-23).\n\nexport type SemanticType =\n | 'pricing' | 'hero' | 'social_proof' | 'cta' | 'features'\n | 'faq' | 'comparison' | 'trust' | 'navigation' | 'generic';\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\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};\n\n// Ordered most-specific first — the first keyword hit wins.\nconst KEYWORDS: Array<[SemanticType, RegExp]> = [\n ['pricing', /\\b(pricing|price|plans?|subscriptions?|per month|\\/mo|tier)\\b/i],\n ['faq', /\\b(faq|frequently asked|common questions?)\\b/i],\n ['comparison', /\\b(compare|comparison|versus|vs\\.)\\b/i],\n ['social_proof', /\\b(testimonial|reviews?|trusted by|loved by|customers?|logos|rated|case stud)\\b/i],\n ['trust', /\\b(security|guarantee|privacy|compliance|secure|gdpr|soc ?2|encrypt)\\b/i],\n ['features', /\\b(features?|how it works|benefits?|capabilit|what you get)\\b/i],\n];\n\n// Content-evidence patterns — run against bodyText. Ordered most-specific first.\n// Deliberately conservative: pricing needs per-period/plan context next to money\n// so an article mentioning \"$5 million\" stays generic.\nconst 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|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\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 or content\n * evidence (trustable enough to auto-apply); `weak` = structural fallback\n * (cta/hero/navigation/generic — capture-worthy but not persona evidence).\n */\nexport function classifyFeatures(f: SectionFeatures): { type: SemanticType; strength: 'strong' | 'weak' } {\n if (f.tag === 'nav' || f.tag === 'footer') return { type: 'navigation', strength: 'weak' };\n const hay = `${f.idClass} ${f.headingText}`.toLowerCase();\n for (const [type, re] of KEYWORDS) {\n if (re.test(hay)) return { type, strength: 'strong' };\n }\n for (const [type, re] of CONTENT_PATTERNS) {\n if (re.test(f.bodyText)) return { type, strength: 'strong' };\n }\n if (f.actionCount >= 1 && f.textLength > 0 && f.textLength < 200) return { type: 'cta', strength: 'weak' };\n if (f.tag === 'header') return { type: 'hero', strength: 'weak' };\n if (/\\b(hero|headline|banner)\\b/i.test(hay)) return { type: 'hero', strength: 'weak' };\n return { type: '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 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 };\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","import { initSession, type SessionConfig, type SessionManager } from './session';\nimport {\n createEventQueue,\n retryStorageKey,\n type EventQueue,\n type EventType,\n type QueueConfig,\n type SentientEvent,\n} from './queue';\nimport {\n createAssignmentCache,\n type Assignment,\n} from './cache';\nimport type {\n GraphConfig,\n GraphSnapshot,\n} from './graph';\nimport {\n detectDeviceClass,\n detectTrafficSource,\n detectTimeOfDay,\n referrerDomainFromReferer,\n uaTokenMatch,\n} from './session-meta.js';\nimport {\n toWireSlot,\n baselineResultFor,\n armOfResult,\n type SlotDeclInput,\n type SlotResult,\n} from './slots.js';\nimport { readSnapshot, writeSnapshot, SNAPSHOT_STORAGE_KEY_PREFIX, type SlotConfigEntry, type CompoundLocator } from './snapshot.js';\nimport { confidenceBand } from '@sentientui/policy';\nimport { createLocalModeClient } from './local-mode.js';\n\nexport { PROD_KEYLESS_ERROR, LOCAL_MODE_BANNER } from './local-mode.js';\n\nexport {\n detectDeviceClass,\n detectTrafficSource,\n detectTimeOfDay,\n deriveSessionSegment,\n referrerDomainFromReferer,\n uaTokenMatch,\n matchedAgentToken,\n agentUaList,\n} from './session-meta.js';\n\nconst DEFAULT_INGEST_URL = 'https://api.sentient-ui.com/v1/events';\n\n// Keyed by apiKey so multiple init() calls (HMR, multi-project) don't collide.\nconst _clients = new Map<string, {\n config: SentientConfig;\n upgrade: ((c: SentientClient) => void) | null;\n}>();\nlet _lastApiKey: string | null = null;\n\nexport type SentientConfig = {\n apiKey: string;\n context: 'landing' | 'ecommerce' | 'saas' | 'marketplace';\n /** @internal — not exposed to users; defaults to the hosted SentientUI API. */\n ingestUrl?: string;\n debug?: boolean;\n /**\n * Pre-seeded assignments from `preloadAssignments()` (SSR).\n * Seeds the local cache so `assign()` returns without a network call for\n * listed code variants, guaranteeing server and client render the same\n * variant on first paint. Managed-text components (assign with no\n * variantIds) still fetch once when the seed carries no content.\n */\n initialAssignments?: Record<string, string>;\n /**\n * Segment used for SSR preload (`device:source`). When set with `initialAssignments`,\n * seeds the assignment cache under this key so hydration matches the server bandit row.\n */\n sessionSegment?: string;\n /**\n * Consent gate. When `false`, returns a no-op client and performs no tracking.\n * Defaults to `true`. Re-call `init()` (via `AdaptiveProvider` consent prop) when\n * the user grants or revokes consent mid-session.\n */\n consent?: boolean;\n /**\n * Behavior before consent is granted. `'statistical_winner'` fetches the\n * best-performing variant via `GET /v1/winner` — no session or tracking data\n * is stored. `'control'` (default) shows `variantIds[0]` with no API call.\n * Applies when tracking is gated off — either `consent: false` or an active\n * Do Not Track signal.\n */\n preConsentBehavior?: 'statistical_winner' | 'control';\n /**\n * Whether to honor the browser's Do Not Track (DNT) signal. Defaults to `true`.\n * When `true` and the visitor has DNT enabled, the SDK sets no cookies and\n * sends no tracking data — behaving exactly as `consent: false` (still serving\n * the read-only `preConsentBehavior` winner if configured), and `grantConsent()`\n * will not upgrade it. Set `false` to make your own consent gate authoritative.\n */\n respectDoNotTrack?: boolean;\n userId?: string;\n /**\n * Session ID generated server-side (from `loadAdaptiveAssignments` / `loadAdaptiveDecision`).\n * When provided, the client adopts this ID on first visit instead of generating a new one,\n * ensuring events and goals are attributed to the same session the server used for assignment.\n */\n ssrSessionId?: string;\n /**\n * ISO 3166-1 alpha-2 country code for the visitor. When provided (e.g. from\n * the `CF-IPCountry` header in a Next.js server component), it is included in\n * the session upsert so country-based segmentation works without client-side\n * geo lookup.\n */\n country?: string;\n /**\n * Pre-seeded slot results from `preloadDecisions()` / `loadAdaptiveDecision()`\n * (SSR). Seeds the local slot state so `getSlotResult()` agrees with the\n * server-rendered markup on first paint.\n */\n initialSlots?: Record<string, SlotResult>;\n /**\n * Persona decided during SSR. Takes priority over the html-attribute\n * adoption and the local snapshot.\n */\n initialPersona?: { persona: string; confidence: number };\n /**\n * Keyless local mode. 'auto' (default) simulates decisions on-device when no\n * valid API key is configured — but only in development builds (the\n * `development` export condition); production bundles physically exclude the\n * engine. `true` forces the local engine regardless of key (escape hatch);\n * `false` restores the silent keyless no-op.\n */\n localMode?: 'auto' | boolean;\n};\n\nexport type AssignResult = { variantId: string; assignmentTtlMs: number; content?: string };\n\nexport type { SlotDeclInput, SlotResult };\nexport { armOfResult, baselineResultFor, baselineSlots, toWireSlot } from './slots.js';\n\nexport {\n SNAPSHOT_STORAGE_KEY_PREFIX,\n readSnapshot,\n writeSnapshot,\n renderPrePaintScript,\n} from './snapshot.js';\nexport type { DecisionSnapshot, SlotConfigEntry, SlotOps, CompoundLocator } from './snapshot.js';\n\n/** An editor-defined goal delivered with a registry-mode decision, for the\n * snippet to install delegated listeners from. */\nexport type GoalDefinition = {\n goalId: string;\n event: 'click' | 'form_submit' | 'url_reached';\n locator?: CompoundLocator;\n urlPattern?: string;\n slotId?: string;\n};\n\nexport type DecideOutcome = {\n layoutOrder: string[] | null;\n assignments: Record<string, string>;\n slots: Record<string, SlotResult>;\n persona: string;\n confidence: number;\n // Registry mode only: where/what to apply for server-defined slots.\n slotConfig?: Record<string, SlotConfigEntry>;\n // Registry mode only: editor-defined goals to wire up.\n goals?: GoalDefinition[];\n};\n\nexport type DecideInput = {\n sections?: string[];\n components?: Array<{ id: string; variantIds?: string[] }>;\n slots?: SlotDeclInput[];\n // 'registry' → serve the project's published slot_definitions in addition to\n // any declared slots (registry wins on id collision). Default 'request'.\n slotsFrom?: 'request' | 'registry';\n /**\n * Caller's build version (e.g. the snippet's `__SNIPPET_VERSION__`), sent\n * as `v` on the wire. Additive/best-effort: the server persists it for\n * version-skew reporting (see apps/api decide route) and ignores it\n * entirely on older deployments. Omit if the caller has no version to report.\n */\n v?: string;\n};\n\nexport type WeightEntry = { variantId: string; pulls: number; avgReward: number | null };\nexport type ComponentWeightEntry = { componentId: string; updatedAt: number; variants: WeightEntry[] };\n\nexport type ComponentGoalOptions = {\n /** Reward credited to the served variant (0–1). Defaults to 1. */\n reward?: number;\n /** Extra fields merged into the event payload. */\n metadata?: Record<string, unknown>;\n};\n\nexport type SentientClient = {\n track(\n event: Omit<SentientEvent, 'id' | 'sessionId' | 'timestamp' | 'timeInSession'>,\n ): void;\n goal(name: string, metadata?: Record<string, unknown>, weight?: number, stepIndex?: number): void;\n /**\n * Records a conversion attributed to the variant currently served for\n * `componentId`, so it feeds the per-variant CVR funnel. Resolves the served\n * variant from the local assignment cache — no need to pass variantId or\n * projectId. No-ops if the component has not been assigned yet (render its\n * `<Adaptive>`/call `assign()` first). Prefer this over bare `goal()` for\n * variant experiments; `goal()` is session-level only (no component attribution).\n */\n componentGoal(componentId: string, goalType: string, opts?: ComponentGoalOptions): void;\n identify(userId: string): void;\n getAssignment(componentId: string, segment: string): Assignment | null;\n /** Server-side variant assignment. Caches the result locally per (component, segment). */\n assign(componentId: string, variantIds?: string[], agentData?: unknown, agentDataByVariant?: Record<string, unknown>): Promise<AssignResult | null>;\n /**\n * Single-roundtrip decision for layout sections, component variants, and\n * adaptive slots. Awaits the session upsert (like `assign`) so the server\n * never decides for a session row that doesn't exist yet. A response\n * without a `slots` field means the server predates slots — every declared\n * slot resolves to its baseline and no retry is made.\n */\n decide(input: DecideInput): Promise<DecideOutcome | null>;\n /** Slot result served this session (decide result, SSR seed, snapshot, or failure baseline). Null when unknown. */\n getSlotResult(slotId: string): SlotResult | null;\n /** Current persona estimate. Band is always `confidenceBand(confidence)`. Null when nothing is known yet. */\n getPersona(): { persona: string; confidence: number; band: 'low' | 'medium' | 'high' } | null;\n /** Fetches current bandit weights for all components in this project. Used by the provider to keep live-weight polling fresh. */\n fetchWeights(): Promise<ComponentWeightEntry[]>;\n getGraph(): GraphSnapshot;\n /**\n * Routine teardown: stops timers/listeners and flushes pending events, but\n * KEEPS the visitor identity, decision snapshot, and retry bucket. Use for\n * component unmount / re-init (framework providers call this on cleanup).\n */\n dispose(): void;\n /**\n * Consent-revocation / forget-me teardown: everything `dispose()` does,\n * plus deletion of the visitor identity (`_snt_uid`), the decision\n * snapshot, and the persisted retry bucket. The next visit starts as a\n * brand-new visitor.\n */\n destroy(): void;\n /** True when this client is the keyless local-mode client (dev only). */\n readonly isLocal?: boolean;\n};\n\n// SSR preload helpers moved to the `@sentientui/core/server` entry in 0.6.0 so\n// ~200 lines of Node-only fetch logic stop shipping in the browser bundle.\n\nexport { attachMicroSignalDetectors } from './micro-signals.js';\nexport type { MicroSignalEmitter, MicroSignalType } from './micro-signals.js';\n\nexport type {\n SessionConfig,\n SessionManager,\n EventType,\n SentientEvent,\n QueueConfig,\n Assignment,\n GraphSnapshot,\n GraphConfig,\n};\n\nfunction generateEventId(): string {\n try {\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID();\n }\n } catch {\n /* fall through */\n }\n // Fallback: RFC 4122 v4 UUID built from Math.random() — avoids invalid UUID\n // syntax that would cause `invalid input syntax for type uuid` on INSERT.\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);\n });\n}\n\nconst SSR_CLIENT: SentientClient = {\n track: () => undefined,\n goal: () => undefined,\n componentGoal: () => undefined,\n identify: () => undefined,\n getAssignment: () => null,\n assign: () => Promise.resolve(null),\n decide: () => Promise.resolve(null),\n getSlotResult: () => null,\n getPersona: () => null,\n fetchWeights: () => Promise.resolve([]),\n getGraph: () => ({ pageNodes: [], capturedAt: 0 }),\n dispose: () => undefined,\n destroy: () => undefined,\n};\n\nfunction readUtmParams(): Record<string, string> {\n try {\n const out: Record<string, string> = {};\n const sp = new URLSearchParams(window.location.search);\n for (const [k, v] of sp) {\n if (k.startsWith('utm_')) out[k] = v;\n }\n return out;\n } catch {\n return {};\n }\n}\n\nfunction deriveBaseUrl(ingestUrl: string): string {\n return ingestUrl.replace(/\\/events\\/?$/, '');\n}\n\n/**\n * Detects whether the visitor has signalled a tracking opt-out. Honors Global\n * Privacy Control (`navigator.globalPrivacyControl`) — the legally-enforceable\n * CCPA/CPRA signal — as well as Do Not Track (`navigator.doNotTrack`, the legacy\n * `window.doNotTrack` on older Firefox, and `navigator.msDoNotTrack` on old\n * IE/Edge). GPC is a boolean; DNT is opt-out only when explicitly `'1'`/`'yes'`.\n */\nexport function isDoNotTrackEnabled(): boolean {\n // GPC is a boolean flag, checked separately from the DNT string signals.\n if (\n typeof navigator !== 'undefined' &&\n (navigator as unknown as { globalPrivacyControl?: boolean }).globalPrivacyControl === true\n ) {\n return true;\n }\n const signals = [\n typeof navigator !== 'undefined' ? navigator.doNotTrack : undefined,\n typeof window !== 'undefined'\n ? (window as unknown as { doNotTrack?: string | null }).doNotTrack\n : undefined,\n typeof navigator !== 'undefined'\n ? (navigator as unknown as { msDoNotTrack?: string | null }).msDoNotTrack\n : undefined,\n ];\n return signals.some((v) => v === '1' || v === 'yes');\n}\n\n/**\n * Upgrades a pre-consent client (created with `consent: false, preConsentBehavior: 'statistical_winner'`)\n * to a fully-tracking client. Call this from your consent management platform callback.\n * For React apps, prefer updating the `consent` prop on `<AdaptiveProvider>`.\n * Pass `apiKey` to target a specific project; omit to upgrade the most-recently-initialized client.\n */\nexport function grantConsent(apiKey?: string): void {\n if (typeof window === 'undefined') return;\n\n const key = apiKey ?? _lastApiKey;\n if (!key) {\n console.warn('[sentient] grantConsent() called before init()');\n return;\n }\n\n const entry = _clients.get(key);\n if (!entry) {\n console.warn('[sentient] grantConsent() called before init()');\n return;\n }\n\n const { config, upgrade } = entry;\n if (!upgrade) return;\n\n // Honor an active Do Not Track signal — consent cannot override a global opt-out.\n if (config.respectDoNotTrack !== false && isDoNotTrackEnabled()) return;\n\n const fullClient = init({ ...config, consent: true });\n upgrade(fullClient);\n _clients.set(key, { config: { ...config, consent: true }, upgrade: null });\n}\n\nfunction createPreConsentProxy(config: SentientConfig): { proxy: SentientClient; setInner: (c: SentientClient) => void } {\n const baseUrl = deriveBaseUrl(config.ingestUrl ?? DEFAULT_INGEST_URL);\n const authHeaders = {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${config.apiKey}`,\n } as const;\n\n let inner: SentientClient = {\n track: () => undefined,\n goal: () => undefined,\n componentGoal: () => undefined,\n identify: () => undefined,\n getAssignment: () => null,\n fetchWeights: () => Promise.resolve([]),\n async assign(componentId, variantIds, _agentData?) {\n try {\n const params = new URLSearchParams({ componentId });\n for (const v of variantIds ?? []) params.append('variantIds[]', v);\n const res = await fetch(`${baseUrl}/winner?${params.toString()}`, {\n headers: authHeaders,\n });\n if (!res.ok) return variantIds?.[0] ? { variantId: variantIds[0], assignmentTtlMs: 0 } : null;\n const body = (await res.json()) as { variantId: string };\n return { variantId: body.variantId, assignmentTtlMs: 0 };\n } catch {\n return variantIds?.[0] ? { variantId: variantIds[0], assignmentTtlMs: 0 } : null;\n }\n },\n decide: () => Promise.resolve(null),\n getSlotResult: () => null,\n getPersona: () => null,\n getGraph: () => ({ pageNodes: [], capturedAt: 0 }),\n dispose: () => undefined,\n destroy: () => undefined,\n };\n\n const proxy: SentientClient = {\n track: (e) => inner.track(e),\n goal: (n, m, w, s) => inner.goal(n, m, w, s),\n componentGoal: (c, g, o) => inner.componentGoal(c, g, o),\n identify: (u) => inner.identify(u),\n getAssignment: (c, s) => inner.getAssignment(c, s),\n assign: (c, v, a, av) => inner.assign(c, v, a, av),\n decide: (i) => inner.decide(i),\n getSlotResult: (s) => inner.getSlotResult(s),\n getPersona: () => inner.getPersona(),\n fetchWeights: () => inner.fetchWeights(),\n getGraph: () => inner.getGraph(),\n dispose: () => inner.dispose(),\n destroy: () => inner.destroy(),\n };\n\n function setInner(fullClient: SentientClient) {\n inner = fullClient;\n }\n\n return { proxy, setInner };\n}\n\n/**\n * Initializes the Sentient client. Returns a no-op client during SSR.\n */\nexport function init(config: SentientConfig): SentientClient {\n if (typeof window === 'undefined') {\n return SSR_CLIENT;\n }\n\n _lastApiKey = config.apiKey;\n\n // DNT/GPC (a global opt-out) or an explicit `consent: false` must be evaluated\n // BEFORE the local-mode branch: createLocalModeClient() calls initSession()\n // unconditionally, so a gated visitor would otherwise be issued the 365-day\n // `_snt_uid` identity cookie in keyless/local mode (audit P2). DNT/GPC also\n // gates tracking off even when the site passes `consent: true`, and blocks\n // `grantConsent()` from upgrading.\n const dntBlocked = config.respectDoNotTrack !== false && isDoNotTrackEnabled();\n const gated = config.consent === false || dntBlocked;\n\n // Keyless local mode. `localMode: true` forces the local engine (documented\n // escape hatch); 'auto' (default) engages it only when no valid key is\n // present. In production builds `@sentientui/core/local` resolves to a stub\n // and this degrades to a no-op client + one console.error per page load\n // (createLocalModeClient handles that), so no NODE_ENV check is needed here.\n const keyValid = typeof config.apiKey === 'string' && config.apiKey.startsWith('pk_');\n if (config.localMode === true || (!keyValid && config.localMode !== false)) {\n // A gated visitor must never get the identity cookie. Local mode has no\n // server to serve a statistical winner from, so return a plain no-op.\n if (gated) {\n _clients.set(config.apiKey || 'local', { config, upgrade: null });\n return SSR_CLIENT;\n }\n _clients.set(config.apiKey || 'local', { config, upgrade: null });\n return createLocalModeClient(config);\n }\n\n if (gated) {\n if (config.preConsentBehavior === 'statistical_winner') {\n if (!config.apiKey || !config.apiKey.startsWith('pk_')) {\n console.warn('[sentient] init() called with an invalid apiKey — expected a pk_ public key. SDK disabled.');\n return SSR_CLIENT;\n }\n const { proxy, setInner } = createPreConsentProxy(config);\n // Under DNT the read-only winner still serves, but consent can never\n // upgrade it to tracking — so drop the upgrade hook.\n _clients.set(config.apiKey, { config, upgrade: dntBlocked ? null : setInner });\n return proxy;\n }\n _clients.set(config.apiKey, { config, upgrade: null });\n return SSR_CLIENT;\n }\n\n if (!config.apiKey || !config.apiKey.startsWith('pk_')) {\n console.warn('[sentient] init() called with an invalid apiKey — expected a pk_ public key. SDK disabled.');\n return SSR_CLIENT;\n }\n\n if (config.ingestUrl === '') {\n console.warn('[sentient] init() called with an empty ingestUrl. SDK disabled.');\n return SSR_CLIENT;\n }\n\n const resolvedIngestUrl = config.ingestUrl ?? DEFAULT_INGEST_URL;\n\n const sessionStart = Date.now();\n const session = initSession({ ssrSessionId: config.ssrSessionId });\n const assignmentCache = createAssignmentCache();\n const eventQueue = createEventQueue({ ingestUrl: resolvedIngestUrl, apiKey: config.apiKey });\n const baseUrl = deriveBaseUrl(resolvedIngestUrl);\n\n const authHeaders = {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${config.apiKey}`,\n } as const;\n\n const deviceClass = detectDeviceClass(navigator.userAgent ?? '');\n const appOrigin = typeof window !== 'undefined' ? window.location.origin : undefined;\n const trafficSource = detectTrafficSource(document.referrer ?? '', appOrigin);\n const sessionSegment =\n config.sessionSegment ?? `${deviceClass}:${trafficSource}`;\n const inflightAssigns = new Map<string, Promise<AssignResult | null>>();\n\n // --- Adaptive-slot state (decide) ---\n // Results served for this session, keyed by slot id. Written by decide();\n // read by getSlotResult() (Task 3.3) and componentGoal's slot fallback.\n const slotStore = new Map<string, SlotResult>();\n let personaState: { persona: string; confidence: number } | null = null;\n\n // On decide failure every declared slot must still resolve — to its baseline.\n // Never overwrite a previously served result.\n const seedSlotBaselines = (decls: SlotDeclInput[]): void => {\n for (const d of decls) {\n if (!slotStore.has(d.id)) slotStore.set(d.id, baselineResultFor(d));\n }\n };\n\n // Seed slot/persona state. Priority: explicit SSR seeds → snapshot.\n if (config.initialSlots) {\n for (const [slotId, result] of Object.entries(config.initialSlots)) {\n slotStore.set(slotId, result);\n }\n }\n const seedSnapshot = readSnapshot(config.apiKey);\n if (seedSnapshot) {\n for (const [slotId, result] of Object.entries(seedSnapshot.slots)) {\n if (!slotStore.has(slotId)) slotStore.set(slotId, result);\n }\n }\n\n // Band-only persona sources (html attrs, snapshot) become a band-consistent\n // numeric confidence so confidenceBand(confidence) always equals the band.\n const BAND_CONFIDENCE: Record<string, number> = { low: 0.15, medium: 0.5, high: 0.85 };\n if (config.initialPersona) {\n personaState = { ...config.initialPersona };\n } else {\n // Single-writer rule: the inline pre-paint script owns the <html>\n // attributes. The client ADOPTS them as truth and never rewrites them\n // mid-session (next visit's script picks up the new snapshot instead).\n const ds = document.documentElement.dataset;\n if (ds.sentientPersona) {\n personaState = {\n persona: ds.sentientPersona,\n confidence: BAND_CONFIDENCE[ds.sentientConfidence ?? 'low'] ?? 0.15,\n };\n } else if (seedSnapshot) {\n personaState = {\n persona: seedSnapshot.persona,\n confidence: BAND_CONFIDENCE[seedSnapshot.band] ?? 0.15,\n };\n }\n }\n\n // Seed SSR-preloaded assignments into the local cache so assign() finds a\n // cache hit immediately — no network call, no variant flash on hydration.\n if (config.initialAssignments) {\n for (const [componentId, variantId] of Object.entries(config.initialAssignments)) {\n assignmentCache.set(componentId, sessionSegment, {\n variantId,\n assignedAt: Date.now(),\n segment: sessionSegment,\n confidence: 1,\n });\n }\n }\n\n // Upsert session metadata once on init. assign() awaits this promise so\n // the server isn't asked to assign for a session row that doesn't exist yet.\n let sessionReady: Promise<void> = Promise.resolve();\n\n const sessionId = session.getSessionId();\n if (sessionId) {\n const referrerDomain = referrerDomainFromReferer(document.referrer ?? '');\n const sessionBody = {\n sessionId,\n deviceClass,\n trafficSource,\n referrerDomain,\n utmParams: readUtmParams(),\n timeOfDay: detectTimeOfDay(new Date()),\n dayOfWeek: ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'][new Date().getDay()],\n ephemeral: session.isEphemeral(),\n // Likely-automation hint: navigator.webdriver (set under automation\n // control) or a known agent token in the UA. Probabilistic — used for\n // metrics + bandit exclusion server-side, never to change what's served.\n automation:\n (typeof navigator !== 'undefined' && navigator.webdriver === true) ||\n uaTokenMatch(navigator.userAgent ?? ''),\n ...(config.userId ? { userId: config.userId } : {}),\n ...(config.country ? { country: config.country } : {}),\n };\n try {\n sessionReady = fetch(`${baseUrl}/sessions`, {\n method: 'POST',\n keepalive: true,\n body: JSON.stringify(sessionBody),\n headers: authHeaders,\n })\n .then((res) => {\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 }\n return undefined;\n })\n .catch(() => undefined);\n } catch {\n /* never throw on init */\n }\n }\n\n if (config.debug) {\n console.log('[sentient] initialized', { context: config.context });\n (\n window as unknown as {\n __sentient?: {\n client: SentientClient;\n queue: EventQueue;\n };\n }\n ).__sentient = {\n client: null as unknown as SentientClient,\n queue: eventQueue,\n };\n }\n\n const client: SentientClient = {\n goal(name, metadata = {}, weight = 1.0, stepIndex = 0) {\n const sid = session.getSessionId();\n if (!sid) return;\n const goalId = generateEventId();\n sessionReady.then(() => {\n fetch(`${baseUrl}/goals`, {\n method: 'POST',\n keepalive: true,\n body: JSON.stringify({ sessionId: sid, name, metadata, weight, stepIndex, goalId }),\n headers: authHeaders,\n }).catch(() => undefined);\n });\n },\n\n componentGoal(componentId, goalType, opts) {\n const sid = session.getSessionId();\n if (!sid) return;\n // Variant experiments resolve from the assignment cache; adaptive slots\n // (useAdaptiveTokens / AdaptiveGroup) resolve from the slot state, using\n // the canonical arm string as the attributed variantId.\n const assignment = assignmentCache.get(componentId, sessionSegment);\n const slotResult = assignment ? null : slotStore.get(componentId) ?? null;\n if (!assignment && slotResult === null) {\n if (config.debug) {\n console.warn(\n `[sentient] componentGoal(\"${componentId}\"): no assignment or slot decision yet — render its <Adaptive>/adaptive hook or call assign()/decide() before recording a goal.`,\n );\n }\n return;\n }\n const attributedVariantId = assignment ? assignment.variantId : armOfResult(slotResult!);\n const fullEvent: SentientEvent = {\n id: generateEventId(),\n sessionId: sid,\n projectId: config.apiKey,\n componentId,\n variantId: attributedVariantId,\n eventType: 'goal_achieved',\n goalType,\n payload: { reward: opts?.reward ?? 1, ...(opts?.metadata ?? {}) },\n timestamp: Date.now(),\n timeInSession: Date.now() - sessionStart,\n };\n if (config.debug) {\n console.log('[sentient] componentGoal', fullEvent);\n }\n sessionReady.then(() => eventQueue.push(fullEvent));\n },\n\n identify(userId) {\n const sid = session.getSessionId();\n if (!sid) return;\n sessionReady.then(() => {\n fetch(`${baseUrl}/sessions`, {\n method: 'POST',\n keepalive: true,\n body: JSON.stringify({ sessionId: sid, userId, ephemeral: session.isEphemeral() }),\n headers: authHeaders,\n }).catch(() => undefined);\n });\n },\n\n track(event) {\n const sessionId = session.getSessionId();\n if (!sessionId) return;\n\n const fullEvent: SentientEvent = {\n ...event,\n id: generateEventId(),\n sessionId,\n timestamp: Date.now(),\n timeInSession: Date.now() - sessionStart,\n };\n\n if (config.debug) {\n console.log('[sentient] track', fullEvent);\n }\n\n sessionReady.then(() => eventQueue.push(fullEvent));\n },\n\n getAssignment(componentId, segment) {\n return assignmentCache.get(componentId, segment);\n },\n\n async assign(componentId, variantIds, agentData?, agentDataByVariant?) {\n const sid = session.getSessionId();\n if (!sid) return null;\n\n const cached = assignmentCache.get(componentId, sessionSegment);\n // When variantIds are provided (A/B code variant), a cache hit is always final.\n // When variantIds are absent (managed text component), only hit the cache if content\n // is present — a seed from initialAssignments has no content and must still fetch.\n if (cached && (variantIds?.length || cached.content !== undefined)) {\n return { variantId: cached.variantId, assignmentTtlMs: 0, content: cached.content };\n }\n\n // Coalesce concurrent assigns for the same component (e.g. several\n // mounted slots sharing one id) into a single network request.\n const inflight = inflightAssigns.get(componentId);\n if (inflight) return inflight;\n\n const request = (async (): Promise<AssignResult | null> => {\n await sessionReady;\n try {\n const body: Record<string, unknown> = { sessionId: sid, componentId, variantIds };\n if (agentDataByVariant !== undefined) body.agentDataByVariant = agentDataByVariant;\n else if (agentData !== undefined) body.agentData = agentData;\n const res = await fetch(`${baseUrl}/assign`, {\n method: 'POST',\n body: JSON.stringify(body),\n headers: authHeaders,\n });\n if (!res.ok) return null;\n const result = (await res.json()) as AssignResult;\n assignmentCache.set(componentId, sessionSegment, {\n variantId: result.variantId,\n assignedAt: Date.now(),\n segment: sessionSegment,\n confidence: 1,\n content: result.content,\n });\n return result;\n } catch {\n return null;\n } finally {\n inflightAssigns.delete(componentId);\n }\n })();\n inflightAssigns.set(componentId, request);\n return request;\n },\n\n async decide(input) {\n const sid = session.getSessionId();\n if (!sid) return null;\n const declared = input.slots ?? [];\n await sessionReady;\n try {\n const body: Record<string, unknown> = { sessionId: sid };\n if (input.sections && input.sections.length > 0) {\n body.sections = input.sections.map((id) => ({ id }));\n }\n body.components = input.components ?? [];\n if (declared.length > 0) body.slots = declared.map(toWireSlot);\n if (input.slotsFrom === 'registry') body.slotsFrom = 'registry';\n if (input.v) body.v = input.v;\n\n const res = await fetch(`${baseUrl}/decide`, {\n method: 'POST',\n body: JSON.stringify(body),\n headers: authHeaders,\n });\n if (!res.ok) {\n seedSlotBaselines(declared);\n return null;\n }\n const data = (await res.json()) as {\n layoutOrder?: string[] | null;\n assignments?: Record<string, string>;\n slots?: Record<string, SlotResult>;\n slotConfig?: Record<string, SlotConfigEntry>;\n goals?: GoalDefinition[];\n persona?: string;\n confidence?: number;\n };\n\n const slots: Record<string, SlotResult> = {};\n for (const d of declared) {\n // `data.slots === undefined` means the server predates the slots\n // contract: serve the declared baseline everywhere, do NOT retry.\n // (Distinct from `slots: {}`, which also falls back per-slot.)\n slots[d.id] = data.slots?.[d.id] ?? baselineResultFor(d);\n }\n // Registry mode: the server returns slots the request never declared.\n // Take them verbatim (classic mode returns only declared slots, so this\n // union is a no-op there — back-compatible).\n if (data.slots) {\n for (const [slotId, result] of Object.entries(data.slots)) {\n if (!(slotId in slots)) slots[slotId] = result;\n }\n }\n for (const [slotId, result] of Object.entries(slots)) slotStore.set(slotId, result);\n personaState = { persona: data.persona ?? 'unknown', confidence: data.confidence ?? 0 };\n\n // Seed component assignments so <Adaptive>/assign() agree with this\n // decide (same shape as the initialAssignments seed above).\n for (const [componentId, variantId] of Object.entries(data.assignments ?? {})) {\n assignmentCache.set(componentId, sessionSegment, {\n variantId,\n assignedAt: Date.now(),\n segment: sessionSegment,\n confidence: 1,\n });\n }\n\n // Persist for the next visit's pre-paint (SPA cache-first pattern).\n writeSnapshot(config.apiKey, {\n v: 1,\n persona: personaState.persona,\n band: confidenceBand(personaState.confidence),\n slots: Object.fromEntries(slotStore),\n layoutOrder: data.layoutOrder ?? null,\n savedAt: Date.now(),\n ...(data.slotConfig ? { slotConfig: data.slotConfig } : {}),\n });\n\n return {\n layoutOrder: data.layoutOrder ?? null,\n assignments: data.assignments ?? {},\n slots,\n persona: personaState.persona,\n confidence: personaState.confidence,\n ...(data.slotConfig ? { slotConfig: data.slotConfig } : {}),\n ...(data.goals ? { goals: data.goals } : {}),\n };\n } catch {\n seedSlotBaselines(declared);\n return null;\n }\n },\n\n getSlotResult(slotId) {\n return slotStore.get(slotId) ?? null;\n },\n\n getPersona() {\n if (!personaState) return null;\n return {\n persona: personaState.persona,\n confidence: personaState.confidence,\n band: confidenceBand(personaState.confidence),\n };\n },\n\n async fetchWeights() {\n try {\n const res = await fetch(`${baseUrl}/weights`, { headers: authHeaders });\n if (!res.ok) return [];\n const data = (await res.json()) as { components: ComponentWeightEntry[] };\n return data.components ?? [];\n } catch {\n return [];\n }\n },\n\n getGraph() {\n return { pageNodes: [], capturedAt: 0 };\n },\n\n dispose() {\n // Stops the flush timer and unload listeners (with a final flush) but\n // leaves identity, snapshot, and retry bucket for the next client.\n eventQueue.destroy();\n if (config.debug) {\n console.log('[sentient] disposed');\n }\n },\n\n destroy() {\n eventQueue.destroy();\n session.destroy();\n // Forget-me must be total: a surviving decision snapshot would\n // re-personalize the next visit via the pre-paint script, and a\n // persisted retry bucket would re-send events for the deleted identity.\n try {\n localStorage.removeItem(SNAPSHOT_STORAGE_KEY_PREFIX + config.apiKey);\n localStorage.removeItem(retryStorageKey(config.apiKey));\n } catch {\n /* storage unavailable — nothing persisted to remove */\n }\n if (config.debug) {\n console.log('[sentient] destroyed');\n }\n },\n };\n\n _clients.set(config.apiKey, { config, upgrade: null });\n\n if (config.debug) {\n const win = window as unknown as { __sentient?: { client: SentientClient } };\n if (win.__sentient) {\n win.__sentient.client = client;\n }\n }\n\n return client;\n}\n","export type MicroSignalEmitter = (\n signalType: 'rage_click' | 'text_copy' | 'scroll_hesitation' | 'tab_loss',\n extra?: Record<string, unknown>,\n) => void;\n\nexport type MicroSignalType = Parameters<MicroSignalEmitter>[0];\n\n/**\n * Attaches passive behavioral detectors to `node`. Calls `emit` when a signal\n * fires. Each signal type fires at most once per call to this function.\n * Returns a cleanup function that removes all listeners.\n */\nexport function attachMicroSignalDetectors(\n emit: MicroSignalEmitter,\n node: Element,\n variantAssignedAt?: number,\n options?: { tabLoss?: boolean },\n): () => void {\n const cleanups: Array<() => void> = [];\n\n // --- Rage click: 3+ clicks within 500ms ---\n {\n const WINDOW_MS = 500;\n const THRESHOLD = 3;\n let firedOnce = false;\n const timestamps: number[] = [];\n\n const onClick = (): void => {\n if (firedOnce) return;\n const now = Date.now();\n timestamps.push(now);\n while (timestamps.length > 0 && now - timestamps[0]! > WINDOW_MS) {\n timestamps.shift();\n }\n if (timestamps.length >= THRESHOLD) {\n firedOnce = true;\n emit('rage_click');\n }\n };\n\n node.addEventListener('click', onClick);\n cleanups.push(() => node.removeEventListener('click', onClick));\n }\n\n // --- Text copy: copy event within node ---\n {\n let firedOnce = false;\n\n const onCopy = (e: Event): void => {\n if (firedOnce) return;\n if (!(e.target instanceof Node)) return;\n if (!node.contains(e.target) && node !== e.target) return;\n firedOnce = true;\n const sel = typeof window !== 'undefined' ? window.getSelection() : null;\n const selectionLength = sel ? sel.toString().length : 0;\n emit('text_copy', { selectionLength });\n };\n\n document.addEventListener('copy', onCopy);\n cleanups.push(() => document.removeEventListener('copy', onCopy));\n }\n\n // --- Scroll hesitation: scroll stops 3s while component visible ---\n {\n let firedOnce = false;\n let isVisible = false;\n let hesitationTimer: ReturnType<typeof setTimeout> | null = null;\n\n const clearHesitation = (): void => {\n if (hesitationTimer !== null) {\n clearTimeout(hesitationTimer);\n hesitationTimer = null;\n }\n };\n\n const startHesitation = (): void => {\n if (firedOnce || !isVisible) return;\n clearHesitation();\n hesitationTimer = setTimeout(() => {\n if (!firedOnce && isVisible) {\n firedOnce = true;\n emit('scroll_hesitation');\n }\n }, 3000);\n };\n\n const onScroll = (): void => {\n clearHesitation();\n startHesitation();\n };\n\n const ioCallback = (entries: IntersectionObserverEntry[]): void => {\n for (const entry of entries) {\n isVisible = entry.intersectionRatio > 0.3;\n if (!isVisible) clearHesitation();\n else startHesitation();\n }\n };\n const io = new IntersectionObserver(ioCallback, { threshold: [0.3] });\n io.observe(node);\n window.addEventListener('scroll', onScroll, { passive: true });\n\n cleanups.push(() => {\n io.disconnect();\n window.removeEventListener('scroll', onScroll);\n clearHesitation();\n });\n }\n\n // --- Tab loss: tab hidden within 15s of variant_assigned ---\n // Document-level (not node-scoped), so callers that attach detectors to many\n // nodes at once (e.g. the snippet's per-option slot signals) can opt out on all\n // but one node to avoid emitting a duplicate tab_loss per node (audit M5).\n if (options?.tabLoss !== false) {\n let firedOnce = false;\n const assignedAt = variantAssignedAt ?? Date.now();\n\n const onVisibility = (): void => {\n if (firedOnce) return;\n if (document.visibilityState !== 'hidden') return;\n const elapsed = Date.now() - assignedAt;\n if (elapsed < 15_000) {\n firedOnce = true;\n emit('tab_loss', { timeOnPage: elapsed });\n }\n };\n\n document.addEventListener('visibilitychange', onVisibility);\n cleanups.push(() => document.removeEventListener('visibilitychange', onVisibility));\n }\n\n return () => {\n for (const c of cleanups) c();\n };\n}\n","import { classifySection, SEMANTIC_TYPES, type SemanticType } from './classify';\nimport { isDoNotTrackEnabled } from '../index.js';\nimport { attachMicroSignalDetectors } from '../micro-signals.js';\n\n// Shared engagement capture (spec 2026-07-22-persona-signal-capture). Detects\n// semantic sections, registers them via /v1/section-map, and records per-section\n// dwell/scroll via IntersectionObserver — emitting the same 'dwell' events the\n// persona pipeline consumes. Used by the React provider (default on) and the\n// no-code snippet. Defense-in-depth: checks DNT internally even though callers\n// gate on consent/DNT too; a missing IntersectionObserver → no-op.\n\ntype CaptureClient = {\n track(event: { projectId: string; componentId: string; eventType: string; payload: Record<string, unknown> }): void;\n};\n\nexport type EngagementCaptureOptions = {\n apiKey: string;\n /** API base, no trailing slash. Defaults to the hosted API. */\n apiBase?: string;\n doc?: Document;\n /**\n * Also attach per-section micro-signal detectors (rage click, text copy,\n * scroll hesitation, tab loss), attributed to the section's `nc-<type>`\n * component. For the no-code snippet, whose pages have no `<Adaptive>`\n * components carrying their own detectors. Default false — the React SDK\n * keeps its per-component detectors and must not double-attach.\n */\n microSignals?: boolean;\n /**\n * Server-served section-map lookup (persona-coverage auto-classification):\n * consulted after explicit `data-sentient-type` markup, before the local\n * heuristic. Return null when the element has no served label.\n */\n typeOf?: (el: Element) => SemanticType | null;\n};\n\nconst SECTION_SELECTOR = 'section, header, footer, nav, main > div, [data-sentient-section]';\n\n/** Skip a section nested inside another candidate section (avoid double count). */\nfunction isNested(el: Element): boolean {\n return el.parentElement?.closest(SECTION_SELECTOR) != null;\n}\n\nfunction registerSections(\n apiKey: string,\n apiBase: string,\n pageUrl: string,\n sections: Array<{ componentId: string; semanticType: SemanticType; source: 'markup' | 'auto' }>,\n): void {\n try {\n void fetch(`${apiBase}/v1/section-map`, {\n method: 'POST',\n keepalive: true,\n headers: { 'content-type': 'application/json', authorization: `Bearer ${apiKey}` },\n body: JSON.stringify({ pageUrl, sections }),\n }).catch(() => undefined);\n } catch {\n /* fail-safe */\n }\n}\n\nconst NOOP = (): void => undefined;\n\nexport function startEngagementCapture(\n client: CaptureClient,\n opts: EngagementCaptureOptions,\n): () => void {\n const doc = opts.doc ?? (typeof document !== 'undefined' ? document : undefined);\n if (!doc || typeof IntersectionObserver === 'undefined') return NOOP;\n if (isDoNotTrackEnabled()) return NOOP;\n // Keyless zero-network contract: capture exists to feed the hosted persona\n // pipeline — with no api key there is nothing to feed, and the section-map\n // registration fetch must never fire.\n if (!opts.apiKey) return NOOP;\n const apiBase = opts.apiBase ?? 'https://api.sentient-ui.com';\n\n const els = Array.from(doc.querySelectorAll(SECTION_SELECTOR)).filter((el) => !isNested(el));\n if (els.length === 0) return NOOP;\n\n // Collapse to one component per semantic type per page (the matrix aggregates\n // by semantic type anyway). Per-element precedence: explicit data-sentient-type\n // markup → served section map (opts.typeOf) → local heuristic.\n const componentOf = new Map<Element, string>();\n const types = new Map<string, SemanticType>();\n const sources = new Map<string, 'markup' | 'auto'>();\n for (const el of els) {\n const explicit = el.getAttribute('data-sentient-type');\n const markup = explicit && (SEMANTIC_TYPES as readonly string[]).includes(explicit)\n ? (explicit as SemanticType)\n : null;\n const type = markup ?? opts.typeOf?.(el) ?? classifySection(el);\n const componentId = `nc-${type}`;\n componentOf.set(el, componentId);\n types.set(componentId, type);\n // Markup wins if the same collapsed component gets both provenances.\n if (markup) sources.set(componentId, 'markup');\n else if (!sources.has(componentId)) sources.set(componentId, 'auto');\n }\n\n const pageUrl = (doc.defaultView ?? (typeof window !== 'undefined' ? window : undefined))?.location?.pathname ?? '/';\n registerSections(opts.apiKey, apiBase, pageUrl, [...types.entries()].map(([componentId, semanticType]) => ({\n componentId, semanticType, source: sources.get(componentId) ?? 'auto',\n })));\n\n // Accumulate visible dwell (ms) + max scroll ratio per component. `intersecting`\n // tracks in-viewport state independently of `enterAt` (the running clock) so a\n // tab-hide can pause the clock and a tab-show can resume it for still-visible\n // sections — IntersectionObserver does not re-fire on visibilitychange.\n const state = new Map<string, { ms: number; scroll: number; enterAt: number | null; intersecting: boolean }>();\n const get = (id: string) => {\n let s = state.get(id);\n if (!s) { s = { ms: 0, scroll: 0, enterAt: null, intersecting: false }; state.set(id, s); }\n return s;\n };\n\n const observer = new IntersectionObserver((entries) => {\n for (const entry of entries) {\n const id = componentOf.get(entry.target);\n if (!id) continue;\n const s = get(id);\n if (entry.isIntersecting) {\n s.intersecting = true;\n s.enterAt = Date.now();\n if (entry.intersectionRatio > s.scroll) s.scroll = entry.intersectionRatio;\n } else {\n s.intersecting = false;\n if (s.enterAt != null) { s.ms += Date.now() - s.enterAt; s.enterAt = null; }\n }\n }\n }, { threshold: [0, 0.25, 0.5, 0.75, 1] });\n for (const el of componentOf.keys()) observer.observe(el);\n\n // Bank accumulated dwell and RESET the accumulators (so a later emit can't\n // double-count) WITHOUT disconnecting — a visitor who hides/re-shows the tab\n // or tab-switches keeps being measured. Pauses the running clock; the tab-show\n // handler restarts it for still-visible sections so hidden time isn't counted.\n const emit = (): void => {\n const now = Date.now();\n for (const [id, s] of state) {\n if (s.enterAt != null) { s.ms += now - s.enterAt; s.enterAt = null; }\n if (s.ms <= 0) continue;\n try {\n client.track({\n projectId: opts.apiKey, // SDK convention: server derives the real project from the key\n componentId: id,\n eventType: 'dwell',\n payload: { dwell_time: Math.round(s.ms), scroll_depth: Number(s.scroll.toFixed(2)) },\n });\n } catch {\n /* fail-safe */\n }\n s.ms = 0;\n }\n };\n\n const onVisibility = (): void => {\n if (doc.hidden) {\n emit(); // bank + pause\n } else {\n const now = Date.now(); // resume the clock for sections still on screen\n for (const s of state.values()) if (s.intersecting) s.enterAt = now;\n }\n };\n const onPageHide = (): void => {\n emit();\n try { observer.disconnect(); } catch { /* ignore */ }\n };\n doc.addEventListener('visibilitychange', onVisibility);\n const win = doc.defaultView ?? (typeof window !== 'undefined' ? window : undefined);\n win?.addEventListener('pagehide', onPageHide);\n\n // Per-section micro-signal detectors (opt-in; see EngagementCaptureOptions).\n // Attributed to the section's nc-<type> id with no variant — they feed the\n // persona attention fallback and auto-discovery, never rewards.\n const detectorCleanups: Array<() => void> = [];\n if (opts.microSignals) {\n for (const [el, componentId] of componentOf) {\n detectorCleanups.push(\n attachMicroSignalDetectors((signalType, extra = {}) => {\n try {\n client.track({\n projectId: opts.apiKey,\n componentId,\n eventType: 'micro_signal',\n payload: { signalType, ...extra },\n });\n } catch {\n /* fail-safe */\n }\n }, el),\n );\n }\n }\n\n // Cleanup: bank any remaining dwell, then detach everything (provider unmount\n // / consent re-init must not leak observers or listeners).\n return () => {\n emit();\n doc.removeEventListener('visibilitychange', onVisibility);\n win?.removeEventListener('pagehide', onPageHide);\n for (const c of detectorCleanups) c();\n try { observer.disconnect(); } catch { /* ignore */ }\n };\n}\n"],"mappings":"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","names":["index_engagement_exports","__export","SEMANTIC_TYPES","classifyFeatures","classifySection","featuresFromElement","startEngagementCapture","__toCommonJS","SEMANTIC_TYPES","KEYWORDS","CONTENT_PATTERNS","headingText","el","_a","h","classifyFeatures","f","hay","type","re","featuresFromElement","_b","text","classifySection","import_policy","attachMicroSignalDetectors","emit","node","variantAssignedAt","options","cleanups","firedOnce","timestamps","onClick","now","onCopy","e","sel","selectionLength","isVisible","hesitationTimer","clearHesitation","startHesitation","onScroll","ioCallback","entries","entry","io","assignedAt","onVisibility","elapsed","isDoNotTrackEnabled","v","SECTION_SELECTOR","isNested","el","_a","registerSections","apiKey","apiBase","pageUrl","sections","e","NOOP","startEngagementCapture","client","opts","_b","_c","_d","_e","_f","_g","_h","_i","doc","isDoNotTrackEnabled","els","componentOf","types","sources","explicit","markup","SEMANTIC_TYPES","type","classifySection","componentId","semanticType","state","get","id","observer","entries","entry","s","emit","now","onVisibility","onPageHide","win","detectorCleanups","attachMicroSignalDetectors","signalType","extra","__spreadValues","c"]}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{a as v,b as B,c as F,d as h}from"./chunk-CD2A55US.mjs";import{g as _,h as K}from"./chunk-
|
|
1
|
+
import{a as v,b as B,c as F,d as h}from"./chunk-CD2A55US.mjs";import{g as _,h as K}from"./chunk-SHGSYP75.mjs";import"./chunk-L5TA3FAB.mjs";import{a as x}from"./chunk-TMCGHANO.mjs";var L="section, header, footer, nav, main > div, [data-sentient-section]";function R(s){var o;return((o=s.parentElement)==null?void 0:o.closest(L))!=null}function V(s,o,r,f){try{fetch(`${o}/v1/section-map`,{method:"POST",keepalive:!0,headers:{"content-type":"application/json",authorization:`Bearer ${s}`},body:JSON.stringify({pageUrl:r,sections:f})}).catch(()=>{})}catch(m){}}var u=()=>{};function $(s,o){var b,A,C,O,I,k,M,N,D;let r=(b=o.doc)!=null?b:typeof document!="undefined"?document:void 0;if(!r||typeof IntersectionObserver=="undefined"||K()||!o.apiKey)return u;let f=(A=o.apiBase)!=null?A:"https://api.sentient-ui.com",m=Array.from(r.querySelectorAll(L)).filter(e=>!R(e));if(m.length===0)return u;let l=new Map,E=new Map,d=new Map;for(let e of m){let t=e.getAttribute("data-sentient-type"),n=t&&v.includes(t)?t:null,i=(O=n!=null?n:(C=o.typeOf)==null?void 0:C.call(o,e))!=null?O:h(e),a=`nc-${i}`;l.set(e,a),E.set(a,i),n?d.set(a,"markup"):d.has(a)||d.set(a,"auto")}let P=(N=(M=(k=(I=r.defaultView)!=null?I:typeof window!="undefined"?window:void 0)==null?void 0:k.location)==null?void 0:M.pathname)!=null?N:"/";V(o.apiKey,f,P,[...E.entries()].map(([e,t])=>{var n;return{componentId:e,semanticType:t,source:(n=d.get(e))!=null?n:"auto"}}));let p=new Map,j=e=>{let t=p.get(e);return t||(t={ms:0,scroll:0,enterAt:null,intersecting:!1},p.set(e,t)),t},y=new IntersectionObserver(e=>{for(let t of e){let n=l.get(t.target);if(!n)continue;let i=j(n);t.isIntersecting?(i.intersecting=!0,i.enterAt=Date.now(),t.intersectionRatio>i.scroll&&(i.scroll=t.intersectionRatio)):(i.intersecting=!1,i.enterAt!=null&&(i.ms+=Date.now()-i.enterAt,i.enterAt=null))}},{threshold:[0,.25,.5,.75,1]});for(let e of l.keys())y.observe(e);let g=()=>{let e=Date.now();for(let[t,n]of p)if(n.enterAt!=null&&(n.ms+=e-n.enterAt,n.enterAt=null),!(n.ms<=0)){try{s.track({projectId:o.apiKey,componentId:t,eventType:"dwell",payload:{dwell_time:Math.round(n.ms),scroll_depth:Number(n.scroll.toFixed(2))}})}catch(i){}n.ms=0}},S=()=>{if(r.hidden)g();else{let e=Date.now();for(let t of p.values())t.intersecting&&(t.enterAt=e)}},w=()=>{g();try{y.disconnect()}catch(e){}};r.addEventListener("visibilitychange",S);let c=(D=r.defaultView)!=null?D:typeof window!="undefined"?window:void 0;c==null||c.addEventListener("pagehide",w);let T=[];if(o.microSignals)for(let[e,t]of l)T.push(_((n,i={})=>{try{s.track({projectId:o.apiKey,componentId:t,eventType:"micro_signal",payload:x({signalType:n},i)})}catch(a){}},e));return()=>{g(),r.removeEventListener("visibilitychange",S),c==null||c.removeEventListener("pagehide",w);for(let e of T)e();try{y.disconnect()}catch(e){}}}export{v as SEMANTIC_TYPES,B as classifyFeatures,h as classifySection,F as featuresFromElement,$ as startEngagementCapture};
|
|
2
2
|
//# sourceMappingURL=index-engagement.mjs.map
|
package/dist/index-graph.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as SentientConfig, m as SentientClient } from './index-
|
|
2
|
-
export { A as AssignResult, a as Assignment, b as AssignmentCache, E as EventQueue, g as EventType, h as GraphClient, i as GraphConfig, j as GraphSnapshot, l as PageNode, Q as QueueConfig, o as SentientEvent, p as SessionConfig, q as SessionManager } from './index-
|
|
1
|
+
import { n as SentientConfig, m as SentientClient } from './index-DqbtJfra.cjs';
|
|
2
|
+
export { A as AssignResult, a as Assignment, b as AssignmentCache, E as EventQueue, g as EventType, h as GraphClient, i as GraphConfig, j as GraphSnapshot, l as PageNode, Q as QueueConfig, o as SentientEvent, p as SessionConfig, q as SessionManager } from './index-DqbtJfra.cjs';
|
|
3
3
|
export { g as deriveSessionSegment, h as detectDeviceClass, i as detectTimeOfDay, j as detectTrafficSource, r as referrerDomainFromReferer } from './session-meta-DU_3mY7U.cjs';
|
|
4
4
|
import '@sentientui/policy';
|
|
5
5
|
|
package/dist/index-graph.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as SentientConfig, m as SentientClient } from './index-
|
|
2
|
-
export { A as AssignResult, a as Assignment, b as AssignmentCache, E as EventQueue, g as EventType, h as GraphClient, i as GraphConfig, j as GraphSnapshot, l as PageNode, Q as QueueConfig, o as SentientEvent, p as SessionConfig, q as SessionManager } from './index-
|
|
1
|
+
import { n as SentientConfig, m as SentientClient } from './index-CTs3AM9E.js';
|
|
2
|
+
export { A as AssignResult, a as Assignment, b as AssignmentCache, E as EventQueue, g as EventType, h as GraphClient, i as GraphConfig, j as GraphSnapshot, l as PageNode, Q as QueueConfig, o as SentientEvent, p as SessionConfig, q as SessionManager } from './index-CTs3AM9E.js';
|
|
3
3
|
export { g as deriveSessionSegment, h as detectDeviceClass, i as detectTimeOfDay, j as detectTrafficSource, r as referrerDomainFromReferer } from './session-meta-DU_3mY7U.js';
|
|
4
4
|
import '@sentientui/policy';
|
|
5
5
|
|
package/dist/index-graph.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{a as v,d as w}from"./chunk-CD2A55US.mjs";import{h as N,j as T}from"./chunk-
|
|
1
|
+
import{a as v,d as w}from"./chunk-CD2A55US.mjs";import{h as N,j as T}from"./chunk-SHGSYP75.mjs";import{d as R,e as P,f as D,g as k,h as G}from"./chunk-L5TA3FAB.mjs";import{a as h,b}from"./chunk-TMCGHANO.mjs";var $=new Set(["SECTION","ARTICLE","MAIN","DIV"]),j="h1, h2, h3",L={scan:async()=>({nodes:[],edges:[],scannedAt:0}),observe:()=>{},getProminenceScore:()=>0,destroy:()=>{}};function S(e,t,o){return o<=t?0:Math.max(0,Math.min(1,(e-t)/(o-t)))}function U(e){var t,o,m;try{let u=e;for(let s of Object.keys(u)){if(!s.startsWith("__reactFiber")&&!s.startsWith("__reactInternalInstance"))continue;let n=u[s],r=(m=(t=n==null?void 0:n.type)==null?void 0:t.displayName)!=null?m:(o=n==null?void 0:n.type)==null?void 0:o.name;if(r&&r.length>1)return r}}catch(u){}}function F(e){let t={};for(let o of Array.from(e.attributes))o.name.startsWith("data-")&&(t[o.name]=o.value);return t}var A=new Set(v);function K(e){let t=e.getAttribute("data-sentient-type");if(t&&A.has(t))return t;let o=e.getAttribute("role");return o&&A.has(o)?o:w(e)}function q(e){var t,o;return(o=(t=e.getAttribute("data-sentient-id"))!=null?t:e.getAttribute("id"))!=null?o:e.tagName.toLowerCase()}function z(e){let t=0,o=e.parentElement;for(;o;)t++,o=o.parentElement;return t}function I(e,t){var m,u,s;let o=e.querySelector(j);return{componentId:q(e),semanticType:K(e),ariaLabel:(m=e.getAttribute("aria-label"))!=null?m:void 0,headingText:(s=(u=o==null?void 0:o.textContent)==null?void 0:u.trim())!=null?s:void 0,isAboveFold:e.getBoundingClientRect().top<window.innerHeight,prominenceScore:t(e),depth:z(e),reactComponentName:U(e),dataAttributes:F(e)}}function x(e){var s;let t=[],o=new Set,m="__root__",u=new Map;for(let[n,r]of e){let i=n.parentElement,c=m;for(;i;){if(e.has(i)){c=e.get(i);let a=e.get(n),g=`${c}->${a}`;!o.has(g)&&c!==a&&(o.add(g),t.push({fromComponentId:c,toComponentId:a,weight:.6}));break}i=i.parentElement}let d=(s=u.get(c))!=null?s:[];d.push(n),u.set(c,d)}for(let n of u.values())if(!(n.length<2))for(let r=0;r<n.length;r++)for(let i=r+1;i<n.length;i++){let c=e.get(n[r]),d=e.get(n[i]);if(c===d)continue;let a=`${c}->${d}::sib`,g=`${d}->${c}::sib`;o.has(a)||(o.add(a),t.push({fromComponentId:c,toComponentId:d,weight:.3})),o.has(g)||(o.add(g),t.push({fromComponentId:d,toComponentId:c,weight:.3}))}return t}function B(e){let t=[],o=new Set,m=new Map;return document.querySelectorAll("[data-sentient-id]").forEach(n=>{if(n instanceof Element&&!o.has(n)){o.add(n);let r=I(n,e);t.push(r),m.set(n,r.componentId)}}),document.querySelectorAll("section, article, main, aside").forEach(n=>{if(!(n instanceof Element)||o.has(n))return;let r=n.hasAttribute("aria-label"),i=n.hasAttribute("data-sentient-id");if(!r&&!i)return;o.add(n);let c=I(n,e);t.push(c),m.set(n,c.componentId)}),{nodes:t,edges:x(m)}}function _(){if(typeof window=="undefined")return L;let e=null,t=0,o=null,m=r=>{try{let i=window.getComputedStyle(r),c=parseFloat(i.fontSize)||12,d=parseFloat(i.zIndex)||0,a=r.getBoundingClientRect(),g=Math.max(a.top,0),p=window.innerHeight||1,l=1/(g/p+1),f=S(c,12,48)*.4+S(l,0,1)*.4+S(d,0,100)*.2;return Math.max(0,Math.min(1,f))}catch(i){return .5}};return{scan:()=>new Promise(r=>{let i=()=>{let{nodes:c,edges:d}=B(m);r({nodes:c,edges:d,scannedAt:Date.now()})};try{typeof requestIdleCallback=="function"?t=requestIdleCallback(i,{timeout:100}):i()}catch(c){i()}}),observe:r=>{o=r;try{e=new MutationObserver(i=>{let c=[],d=new Map;for(let a of i)a.type==="childList"&&a.addedNodes.forEach(g=>{if(!(g instanceof Element)||!$.has(g.tagName))return;let p=g.hasAttribute("data-sentient-id"),l=g.hasAttribute("aria-label");if(!p&&!l)return;let f=I(g,m);c.push(f),d.set(g,f.componentId)});c.length>0&&o&&o({nodes:c,edges:x(d),addedAt:Date.now()})}),e.observe(document.body,{childList:!0,subtree:!0})}catch(i){}},getProminenceScore:m,destroy:()=>{if(e&&(e.disconnect(),e=null),t&&typeof cancelIdleCallback=="function")try{cancelIdleCallback(t)}catch(r){}t=0,o=null}}}var E="_snt_graph_nodes",O="_snt_graph_edges",H={pricing:["features","faq"],features:["pricing"],faq:["pricing"],social_proof:["cta"],cta:["social_proof","hero","trust"],hero:["cta"],comparison:["pricing"],trust:["cta"]};function J(e){var t;return(t=H[e])!=null?t:[]}function V(e,t){try{let o=localStorage.getItem(e);return o?JSON.parse(o):t}catch(o){return t}}function Y(e,t){try{localStorage.setItem(e,JSON.stringify(t))}catch(o){}}var W=new Set(["pricing","hero","social_proof","cta","features","faq","comparison","trust","navigation","generic"]);function Q(e){return W.has(e)?e:"generic"}function X(e,t,o){let m=`${e}:${t}:${o.join(",")}`,u=5381;for(let s=0;s<m.length;s++)u=(u<<5)+u+m.charCodeAt(s)&4294967295;return(u>>>0).toString(16).padStart(8,"0")}function M(e){let t=new Map,o=new Map,m=()=>{typeof window!="undefined"&&Y(E,[...t.values()])},u=s=>{var n;try{let r=JSON.parse(s);t.clear();for(let i of(n=r.pageNodes)!=null?n:[])t.set(i.componentId,i)}catch(r){}};if(typeof window!="undefined"){let s=V(E,[]);for(let n of s)t.set(n.componentId,n);try{localStorage.removeItem(O)}catch(n){}}return{addPageNode(s){t.set(s.componentId,s),m()},addStructuralEdge(s){let n=`${s.fromComponentId}->${s.toComponentId}`;o.set(n,s)},syncOnce(){var n,r;if(!(e!=null&&e.syncUrl)||typeof window=="undefined")return;let s=[...t.values()];if(s.length!==0)try{let i=new Map;for(let p of s){let l=(n=i.get(p.semanticType))!=null?n:[];l.push(p),i.set(p.semanticType,l)}let c=[],d=new Set;for(let p of s)for(let l of J(p.semanticType)){let f=(r=i.get(l))!=null?r:[];for(let y of f){if(y.componentId===p.componentId)continue;let C=`semantic:${p.componentId}->${y.componentId}`;d.has(C)||(d.add(C),c.push({fromComponentId:p.componentId,toComponentId:y.componentId,type:"semantic",weight:.4,confidence:.9}))}}let a=new Set(s.map(p=>p.componentId));for(let p of o.values()){if(!a.has(p.fromComponentId)||!a.has(p.toComponentId))continue;let l=`structural:${p.fromComponentId}->${p.toComponentId}`;d.has(l)||(d.add(l),c.push({fromComponentId:p.fromComponentId,toComponentId:p.toComponentId,type:"structural",weight:p.weight,confidence:1}))}let g={pageUrl:window.location.href,nodes:s.map(p=>{let l=Q(p.semanticType);return{componentId:p.componentId,semanticType:l,answers:p.answers,contentHash:X(p.componentId,l,p.answers),prominenceScore:p.prominenceScore,depthInPage:p.depth}}),edges:c};fetch(e.syncUrl,{method:"POST",keepalive:!0,headers:h({"Content-Type":"application/json"},e.apiKey?{Authorization:`Bearer ${e.apiKey}`}:{}),body:JSON.stringify(g)}).catch(()=>{})}catch(i){}},snapshot(){return{pageNodes:[...t.values()],capturedAt:Date.now()}},serialize(){return JSON.stringify({pageNodes:[...t.values()]})},restore:u,destroy(){if(typeof window!="undefined")try{localStorage.removeItem(E),localStorage.removeItem(O)}catch(s){}}}}var Z="https://api.sentient-ui.com/v1/events";function ee(){try{let e=document.cookie.match(/(?:^|; )_snt_uid=([^;]*)/);return e?decodeURIComponent(e[1]):void 0}catch(e){return}}function ce(e){var c;let t=T(e),o=e.respectDoNotTrack!==!1&&N(),m=e.consent===!1||o;if(!e.graph||!e.apiKey||m||typeof window=="undefined")return t;let u=_(),s=(c=e.ingestUrl)!=null?c:Z,n=M({syncUrl:s.replace(/\/events\/?$/,"/graph/sync"),apiKey:e.apiKey,projectId:e.apiKey,sessionId:ee()});try{let d=localStorage.getItem("_snt_graph_nodes");d&&n.restore(JSON.stringify({pageNodes:JSON.parse(d)}))}catch(d){}u.scan().then(d=>{for(let a of d.nodes)n.addPageNode({id:a.componentId,componentId:a.componentId,semanticType:a.semanticType,answers:a.headingText?[a.headingText]:[],prominenceScore:a.prominenceScore,depth:a.depth});for(let a of d.edges)n.addStructuralEdge(a);n.syncOnce()});let r=null,i=()=>{r!==null&&clearTimeout(r),r=setTimeout(()=>{r=null,n.syncOnce()},500)};return u.observe(d=>{for(let a of d.nodes)n.addPageNode({id:a.componentId,componentId:a.componentId,semanticType:a.semanticType,answers:a.headingText?[a.headingText]:[],prominenceScore:a.prominenceScore,depth:a.depth});for(let a of d.edges)n.addStructuralEdge(a);i()}),b(h({},t),{getGraph:()=>n.snapshot(),dispose:()=>{r!==null&&clearTimeout(r),u.destroy(),n.destroy(),t.dispose()},destroy:()=>{r!==null&&clearTimeout(r),u.destroy(),n.destroy(),t.destroy()}})}export{G as deriveSessionSegment,R as detectDeviceClass,k as detectTimeOfDay,P as detectTrafficSource,ce as init,D as referrerDomainFromReferer};
|
|
2
2
|
//# sourceMappingURL=index-graph.mjs.map
|
package/dist/index-local.d.cts
CHANGED
package/dist/index-local.d.ts
CHANGED
package/dist/index.d.cts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { A as AssignResult, a as Assignment, C as ComponentGoalOptions, c as ComponentWeightEntry, d as CompoundLocator, D as DecideInput, e as DecideOutcome, f as DecisionSnapshot, g as EventType, G as GoalDefinition, i as GraphConfig, j as GraphSnapshot, L as LOCAL_MODE_BANNER, M as MicroSignalEmitter, k as MicroSignalType, P as PROD_KEYLESS_ERROR, Q as QueueConfig, S as SNAPSHOT_STORAGE_KEY_PREFIX, m as SentientClient, n as SentientConfig, o as SentientEvent, p as SessionConfig, q as SessionManager, r as SlotConfigEntry, s as SlotOps, W as WeightEntry, t as attachMicroSignalDetectors, u as grantConsent, v as init, w as isDoNotTrackEnabled, x as readSnapshot, y as renderPrePaintScript, z as writeSnapshot } from './index-
|
|
1
|
+
export { A as AssignResult, a as Assignment, C as ComponentGoalOptions, c as ComponentWeightEntry, d as CompoundLocator, D as DecideInput, e as DecideOutcome, f as DecisionSnapshot, g as EventType, G as GoalDefinition, i as GraphConfig, j as GraphSnapshot, L as LOCAL_MODE_BANNER, M as MicroSignalEmitter, k as MicroSignalType, P as PROD_KEYLESS_ERROR, Q as QueueConfig, S as SNAPSHOT_STORAGE_KEY_PREFIX, m as SentientClient, n as SentientConfig, o as SentientEvent, p as SessionConfig, q as SessionManager, r as SlotConfigEntry, s as SlotOps, W as WeightEntry, t as attachMicroSignalDetectors, u as grantConsent, v as init, w as isDoNotTrackEnabled, x as readSnapshot, y as renderPrePaintScript, z as writeSnapshot } from './index-DqbtJfra.cjs';
|
|
2
2
|
export { a as SlotDeclInput, b as agentUaList, c as armOfResult, d as baselineResultFor, e as baselineSlots, g as deriveSessionSegment, h as detectDeviceClass, i as detectTimeOfDay, j as detectTrafficSource, m as matchedAgentToken, r as referrerDomainFromReferer, t as toWireSlot, u as uaTokenMatch } from './session-meta-DU_3mY7U.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, C as ComponentGoalOptions, c as ComponentWeightEntry, d as CompoundLocator, D as DecideInput, e as DecideOutcome, f as DecisionSnapshot, g as EventType, G as GoalDefinition, i as GraphConfig, j as GraphSnapshot, L as LOCAL_MODE_BANNER, M as MicroSignalEmitter, k as MicroSignalType, P as PROD_KEYLESS_ERROR, Q as QueueConfig, S as SNAPSHOT_STORAGE_KEY_PREFIX, m as SentientClient, n as SentientConfig, o as SentientEvent, p as SessionConfig, q as SessionManager, r as SlotConfigEntry, s as SlotOps, W as WeightEntry, t as attachMicroSignalDetectors, u as grantConsent, v as init, w as isDoNotTrackEnabled, x as readSnapshot, y as renderPrePaintScript, z as writeSnapshot } from './index-
|
|
1
|
+
export { A as AssignResult, a as Assignment, C as ComponentGoalOptions, c as ComponentWeightEntry, d as CompoundLocator, D as DecideInput, e as DecideOutcome, f as DecisionSnapshot, g as EventType, G as GoalDefinition, i as GraphConfig, j as GraphSnapshot, L as LOCAL_MODE_BANNER, M as MicroSignalEmitter, k as MicroSignalType, P as PROD_KEYLESS_ERROR, Q as QueueConfig, S as SNAPSHOT_STORAGE_KEY_PREFIX, m as SentientClient, n as SentientConfig, o as SentientEvent, p as SessionConfig, q as SessionManager, r as SlotConfigEntry, s as SlotOps, W as WeightEntry, t as attachMicroSignalDetectors, u as grantConsent, v as init, w as isDoNotTrackEnabled, x as readSnapshot, y as renderPrePaintScript, z as writeSnapshot } from './index-CTs3AM9E.js';
|
|
2
2
|
export { a as SlotDeclInput, b as agentUaList, c as armOfResult, d as baselineResultFor, e as baselineSlots, g as deriveSessionSegment, h as detectDeviceClass, i as detectTimeOfDay, j as detectTrafficSource, m as matchedAgentToken, r as referrerDomainFromReferer, t as toWireSlot, u as uaTokenMatch } from './session-meta-DU_3mY7U.js';
|
|
3
3
|
export { SlotResult } from '@sentientui/policy';
|