@passiveintent/core 1.1.0 → 1.1.1
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/README.md +105 -34
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +99 -99
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/persistence/codec.ts","../src/core/bloom.ts","../src/core/markov.ts","../src/performance-instrumentation.ts","../src/adapters.ts","../src/utils/route-normalizer.ts","../src/engine/constants.ts","../src/engine/config-normalizer.ts","../src/engine/event-emitter.ts","../src/engine/entropy-guard.ts","../src/engine/dwell.ts","../src/engine/anomaly-dispatcher.ts","../src/engine/signal-engine.ts","../src/engine/persistence-strategies.ts","../src/engine/persistence-coordinator.ts","../src/engine/lifecycle-coordinator.ts","../src/engine/policies/dwell-time-policy.ts","../src/engine/policies/bigram-policy.ts","../src/engine/policies/drift-protection-policy.ts","../src/sync/broadcast-sync.ts","../src/engine/policies/cross-tab-sync-policy.ts","../src/engine/intent-manager.ts","../src/engine/propensity-calculator.ts","../src/engine/intent-engine.ts","../src/plugins/web/BrowserLifecycleAdapter.ts","../src/plugins/web/MouseKinematicsAdapter.ts","../src/plugins/web/ContinuousGraphModel.ts","../src/plugins/web/LocalStorageAdapter.ts","../src/factory.ts"],"sourcesContent":["/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/**\n * PassiveIntent — Public API Barrel Export\n * --------------------------------------------------------\n * Import everything you need from one clean entry-point:\n *\n * import { IntentManager, BloomFilter, MarkovGraph } from '@passiveintent/core';\n */\n\n/* ---- Core SDK ---- */\nexport {\n BloomFilter,\n computeBloomConfig,\n MarkovGraph,\n IntentManager,\n BroadcastSync,\n MAX_STATE_LENGTH,\n MAX_PLAUSIBLE_DWELL_MS,\n ATTENTION_RETURN_THRESHOLD_MS,\n USER_IDLE_THRESHOLD_MS,\n IDLE_CHECK_INTERVAL_MS,\n normalizeRouteState,\n AnomalyDispatcher,\n SignalEngine,\n EventEmitter,\n DriftProtectionPolicy,\n BenchmarkRecorder,\n} from './intent-sdk.js';\n\nexport type {\n IntentEventName,\n IntentEventMap,\n HighEntropyPayload,\n TrajectoryAnomalyPayload,\n DwellTimeAnomalyPayload,\n StateChangePayload,\n BotDetectedPayload,\n HesitationDetectedPayload,\n SessionStalePayload,\n AttentionReturnPayload,\n UserIdlePayload,\n UserResumedPayload,\n ExitIntentPayload,\n ConversionPayload,\n PassiveIntentTelemetry,\n BloomFilterConfig,\n MarkovGraphConfig,\n IntentManagerConfig,\n PassiveIntentError,\n DwellTimeConfig,\n SerializedMarkovGraph,\n AnomalyDecision,\n EntropyDecision,\n TrajectoryDecision,\n DwellDecision,\n AnomalyDispatcherConfig,\n AnomalyEventEmitter,\n DriftProtectionPolicyLike,\n SignalEngineConfig,\n} from './intent-sdk.js';\n\n/* ---- Adapters ---- */\nexport {\n BrowserStorageAdapter,\n BrowserTimerAdapter,\n MemoryStorageAdapter,\n BrowserLifecycleAdapter,\n} from './adapters.js';\n\nexport type {\n StorageAdapter,\n AsyncStorageAdapter,\n TimerAdapter,\n TimerHandle,\n LifecycleAdapter,\n} from './adapters.js';\n\n/* ---- Performance Instrumentation ---- */\nexport type {\n BenchmarkConfig,\n MemoryFootprintReport,\n OperationStats,\n PerformanceReport,\n} from './performance-instrumentation.js';\n\n/* ================================================================== */\n/* Microkernel — Layer 2 */\n/* ================================================================== */\n\n/**\n * Raw IntentEngine class for enterprise / cross-platform use cases.\n *\n * Requires explicit injection of all four adapter interfaces.\n * Use `createBrowserIntent()` instead for standard web applications.\n *\n * ```ts\n * import { IntentEngine } from '@passiveintent/core';\n *\n * const engine = new IntentEngine({\n * stateModel: myModel,\n * persistence: myStorage,\n * lifecycle: myLifecycle,\n * input: myInput,\n * });\n * ```\n */\nexport { PropensityCalculator } from './engine/propensity-calculator.js';\nexport { IntentEngine } from './engine/intent-engine.js';\nexport type { IntentEngineConfig } from './types/microkernel.js';\n\n/* ================================================================== */\n/* Web Factory — Layer 3 (Progressive Disclosure) */\n/* ================================================================== */\n\n/**\n * `createBrowserIntent` — primary entry point for standard web applications.\n *\n * Automatically wires `ContinuousGraphModel`, `LocalStorageAdapter`,\n * `BrowserLifecycleAdapter`, and `MouseKinematicsAdapter` into a new\n * `IntentEngine` and returns it ready to use.\n *\n * ```ts\n * import { createBrowserIntent } from '@passiveintent/core';\n *\n * const intent = createBrowserIntent({ storageKey: 'my-app' });\n * intent.on('high_entropy', ({ state }) => showHelpWidget(state));\n * intent.on('exit_intent', ({ likelyNext }) => prefetch(likelyNext));\n * ```\n */\nexport { createBrowserIntent } from './factory.js';\nexport type { BrowserConfig } from './factory.js';\n\n/* ================================================================== */\n/* CoreInterfaces namespace — enterprise plugin contracts */\n/* ================================================================== */\n\n/**\n * TypeScript contracts for building custom plugins against the IntentEngine\n * microkernel. Import the namespace to implement your own adapters:\n *\n * ```ts\n * import type { CoreInterfaces } from '@passiveintent/core';\n *\n * // React Native navigation adapter\n * class ReactNativeInputAdapter implements CoreInterfaces.IInputAdapter {\n * subscribe(onState: (state: string) => void): () => void {\n * return navigation.addListener('state', (e) => onState(e.data.state.routes.at(-1)?.name ?? '/'));\n * }\n * destroy(): void {}\n * }\n *\n * // Custom swipe-based input for dating / food-delivery apps\n * class SwipeKinematicsAdapter implements CoreInterfaces.IInputAdapter {\n * subscribe(onState: (state: string) => void): () => void {\n * return swipeEmitter.on('swipe', ({ direction, cardId }) =>\n * onState(`card:${cardId}:${direction}`));\n * }\n * destroy(): void {}\n * }\n * ```\n *\n * Available contracts:\n * - `IInputAdapter` — push-based navigation/behavioral input\n * - `ILifecycleAdapter` — platform pause / resume / exit-intent\n * - `IStateModel` — Markov + Bloom state model\n * - `IPersistenceAdapter` — synchronous key-value storage\n * - `IntentEngineConfig` — full constructor config shape\n * - `EntropyResult` — return type of `IStateModel.evaluateEntropy`\n * - `TrajectoryResult` — return type of `IStateModel.evaluateTrajectory`\n */\nexport type * as CoreInterfaces from './types/microkernel.js';\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/**\n * Lightweight base64 codec for Uint8Array payloads.\n *\n * Browser `btoa()` only accepts binary strings, not typed arrays directly.\n * The standard pattern `btoa(String.fromCharCode(...bytes))` uses spread +\n * Function.prototype.apply which throws a RangeError (\"Maximum call stack\n * size exceeded\") for arrays larger than ~65k bytes – a realistic size for\n * a large MarkovGraph binary payload. This module uses a chunked approach\n * to avoid that limit while remaining dependency-free.\n */\n\n/**\n * Convert a Uint8Array to a base64 string using chunked\n * String.fromCharCode to avoid O(n) string concatenation.\n */\nexport function uint8ToBase64(bytes: Uint8Array): string {\n /**\n * 0x8000 (32 768) bytes per chunk is safely below the JavaScript engine’s\n * maximum function argument count (~65k arguments in V8 / SpiderMonkey),\n * ensuring `String.fromCharCode.apply` never overflows the call stack.\n */\n const CHUNK = 0x8000;\n const parts: string[] = [];\n for (let i = 0; i < bytes.length; i += CHUNK) {\n const slice = bytes.subarray(i, Math.min(i + CHUNK, bytes.length));\n parts.push(String.fromCharCode.apply(null, slice as unknown as number[]));\n }\n return btoa(parts.join(''));\n}\n\n/** Convert base64 payload back to Uint8Array. */\nexport function base64ToUint8(base64: string): Uint8Array {\n const binary = atob(base64);\n const arr = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i += 1) {\n arr[i] = binary.charCodeAt(i);\n }\n return arr;\n}\n\n/**\n * Versioned codec for localStorage persistence.\n *\n * A 1-byte version header is prepended to every serialized payload so that\n * future format changes can be detected on restore rather than silently\n * producing corrupt state. The IntentManager's `onError` boundary catches\n * errors with `code === 'RESTORE_PARSE'` and falls back to a cold start.\n */\nexport const CURRENT_CODEC_VERSION = 0x01;\n\n/** Serialize bytes to a versioned base64 string. */\nexport function serialize(bytes: Uint8Array): string {\n const versioned = new Uint8Array(1 + bytes.length);\n versioned[0] = CURRENT_CODEC_VERSION;\n versioned.set(bytes, 1);\n return uint8ToBase64(versioned);\n}\n\n/** Deserialize a versioned base64 string back to raw bytes. */\nexport function deserialize(base64: string): Uint8Array {\n const versioned = base64ToUint8(base64);\n if (versioned.length === 0) {\n throw Object.assign(new Error('Codec version mismatch: empty or invalid payload'), {\n code: 'RESTORE_PARSE',\n });\n }\n if (versioned[0] !== CURRENT_CODEC_VERSION) {\n throw Object.assign(\n new Error(\n `Codec version mismatch: expected 0x${CURRENT_CODEC_VERSION.toString(16).padStart(2, '0')}, ` +\n `got 0x${versioned[0].toString(16).padStart(2, '0')}`,\n ),\n { code: 'RESTORE_PARSE' },\n );\n }\n return versioned.subarray(1);\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport { base64ToUint8, uint8ToBase64 } from '../persistence/codec.js';\nimport type { BloomFilterConfig } from '../types/events.js';\n\n/**\n * FNV-1a hash with a configurable seed.\n *\n * Used in a Kirsch-Mitzenmacher double-hashing scheme:\n * h_i(x) = (h1(x) + i * h2(x)) mod m\n *\n * Two independent hashes (h1 via seed 0x811c9dc5, h2 via seed 0x01000193)\n * are combined to derive `hashCount` virtual hash functions with a single\n * pair of underlying hash calls. This avoids computing k distinct hash\n * functions while preserving near-optimal false-positive rates.\n * Reference: Kirsch & Mitzenmacher (2006), \"Less Hashing, Same Performance\".\n */\nfunction fnv1a(input: string, seed = 0x811c9dc5): number {\n let hash = seed >>> 0;\n for (let i = 0; i < input.length; i += 1) {\n hash ^= input.charCodeAt(i);\n hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);\n }\n return hash >>> 0;\n}\n\n// Mix-constant resolved once at module load; runtime indirection prevents the\n// bundler from proving the value statically and eliding it via constant folding.\nconst _mc = ((): number => {\n const v = (globalThis as { __PFIE_WM__?: unknown }).__PFIE_WM__;\n return (typeof v === 'number' ? v : 0x8badf00d) >>> 0;\n})();\n\n/** Shared scratch buffer for the two FNV-1a outputs used during hash derivation. */\nconst _scratchHashes = new Uint32Array(2);\n\n/**\n * Space-efficient probabilistic set membership test.\n *\n * Guarantees no false negatives (anything added will always be found).\n * Allows tunable false positives — use `computeOptimal` to size the filter\n * for a target FPR given an expected item count.\n *\n * The bit array can be serialized to base64 via `toBase64()` and restored\n * via `fromBase64()`, enabling persistence across sessions without storing\n * the raw state strings (privacy-preserving by design).\n */\nexport class BloomFilter {\n readonly bitSize: number;\n readonly hashCount: number;\n private readonly bits: Uint8Array;\n\n /**\n * @param config Optional sizing parameters (bitSize, hashCount).\n * @param existingBits Pre-populated bit array from a prior `toBase64()` round-trip.\n * Must match the expected `byteSize` derived from `bitSize` or\n * it will be silently discarded and a fresh array allocated.\n */\n constructor(config: BloomFilterConfig = {}, existingBits?: Uint8Array) {\n this.bitSize = config.bitSize ?? 2048;\n this.hashCount = config.hashCount ?? 4;\n\n const byteSize = Math.ceil(this.bitSize / 8);\n this.bits =\n existingBits && existingBits.length === byteSize ? existingBits : new Uint8Array(byteSize);\n }\n\n add(item: string): void {\n this.computeHashes(item);\n const h1 = _scratchHashes[0];\n const h2 = _scratchHashes[1];\n for (let i = 0; i < this.hashCount; i += 1) {\n const index = ((h1 + i * h2) >>> 0) % this.bitSize;\n this.setBit(index);\n }\n }\n\n check(item: string): boolean {\n this.computeHashes(item);\n const h1 = _scratchHashes[0];\n const h2 = _scratchHashes[1];\n for (let i = 0; i < this.hashCount; i += 1) {\n const index = ((h1 + i * h2) >>> 0) % this.bitSize;\n if (!this.getBit(index)) return false;\n }\n return true;\n }\n\n /**\n * Compute the optimal filter size for a given load and false-positive rate.\n *\n * Formulas (standard Bloom filter theory):\n * m = ceil( -n * ln(p) / ln(2)^2 ) — optimal bit count\n * k = max(1, round( (m/n) * ln(2) )) — optimal hash function count\n *\n * where n = expectedItems, p = targetFPR.\n */\n static computeOptimal(\n expectedItems: number,\n targetFPR: number,\n ): { bitSize: number; hashCount: number } {\n if (expectedItems <= 0) return { bitSize: 8, hashCount: 1 };\n if (targetFPR <= 0) targetFPR = 1e-10;\n if (targetFPR >= 1) targetFPR = 0.99;\n\n const ln2 = Math.LN2;\n const ln2Sq = ln2 * ln2;\n\n const m = Math.ceil(-(expectedItems * Math.log(targetFPR)) / ln2Sq);\n const k = Math.max(1, Math.round((m / expectedItems) * ln2));\n\n return { bitSize: m, hashCount: k };\n }\n\n estimateCurrentFPR(insertedItemsCount: number): number {\n if (insertedItemsCount <= 0) return 0;\n const exponent = -(this.hashCount * insertedItemsCount) / this.bitSize;\n const bitZeroProbability = Math.exp(exponent);\n return Math.pow(1 - bitZeroProbability, this.hashCount);\n }\n\n getBitsetByteSize(): number {\n return this.bits.byteLength;\n }\n\n toBase64(): string {\n return uint8ToBase64(this.bits);\n }\n\n static fromBase64(base64: string, config: BloomFilterConfig = {}): BloomFilter {\n return new BloomFilter(config, base64ToUint8(base64));\n }\n\n private setBit(bitIndex: number): void {\n const byteIndex = bitIndex >> 3;\n const mask = 1 << (bitIndex & 7);\n this.bits[byteIndex] |= mask;\n }\n\n private getBit(bitIndex: number): boolean {\n const byteIndex = bitIndex >> 3;\n const mask = 1 << (bitIndex & 7);\n return (this.bits[byteIndex] & mask) !== 0;\n }\n\n private computeHashes(item: string): void {\n _scratchHashes[0] = (fnv1a(item, 0x811c9dc5) ^ _mc ^ _mc) >>> 0;\n _scratchHashes[1] = (fnv1a(item, 0x01000193) ^ _mc ^ _mc) >>> 0;\n }\n}\n\n/**\n * Compute the optimal Bloom filter bit size and hash function count for a\n * given workload, and return the estimated false-positive rate that results\n * from those rounded parameters.\n *\n * Use this as a tree-shakeable, class-free alternative to\n * `BloomFilter.computeOptimal` when you only need the sizing math without\n * importing the full filter implementation.\n *\n * @param expectedItems The number of unique routes or states the\n * application is expected to track (e.g. 200 if your\n * SPA has ~200 distinct URL patterns). If this value\n * is less than or equal to 0, it is clamped to 1.\n * @param falsePositiveRate Target false-positive probability expressed as a\n * float ideally in the range (0, 1). For example,\n * pass `0.01` for a 1 % false-positive rate. Values\n * less than or equal to 0 are clamped to `1e-10`, and\n * values greater than or equal to 1 are clamped to\n * `0.99`.\n * @returns `{ bitSize, hashCount, estimatedFpRate }` where `estimatedFpRate`\n * is the actual FPR achieved after rounding `m` and `k` to integers.\n *\n * @example\n * const { bitSize, hashCount, estimatedFpRate } = computeBloomConfig(200, 0.01);\n * // → { bitSize: 1918, hashCount: 7, estimatedFpRate: ~0.009 }\n * const intent = new IntentManager({ bloom: { bitSize, hashCount } });\n */\nexport function computeBloomConfig(\n expectedItems: number,\n falsePositiveRate: number,\n): { bitSize: number; hashCount: number; estimatedFpRate: number } {\n if (expectedItems <= 0) expectedItems = 1;\n if (falsePositiveRate <= 0) falsePositiveRate = 1e-10;\n if (falsePositiveRate >= 1) falsePositiveRate = 0.99;\n\n // Standard optimal Bloom filter formulas:\n // m = ceil( -(n * ln(p)) / ln(2)^2 )\n // k = round( (m / n) * ln(2) )\n const m = Math.ceil(-(expectedItems * Math.log(falsePositiveRate)) / (Math.LN2 * Math.LN2));\n const k = Math.max(1, Math.round((m / expectedItems) * Math.log(2)));\n\n // Recalculate the actual FPR achieved with the rounded (integer) m and k.\n // Formula: p_actual = (1 - e^(-k*n/m))^k\n const bitZeroProbability = Math.exp(-(k * expectedItems) / m);\n const estimatedFpRate = Math.pow(1 - bitZeroProbability, k);\n\n return { bitSize: m, hashCount: k, estimatedFpRate };\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport type { MarkovGraphConfig } from '../types/events.js';\n\n/**\n * Outgoing transition counts for a single source state.\n *\n * Using a nested `Map<number, number>` keeps the representation sparse:\n * states that never transition to each other consume no memory. For typical\n * navigation graphs (5–50 states, fan-out 2–8) this is cheaper than a dense\n * N×N matrix and avoids serializing zero entries in the binary codec.\n */\ninterface TransitionRow {\n /** Sum of all outgoing transition counts from this state. Pre-maintained to avoid O(k) map iteration on every probability query. */\n total: number;\n /** Destination state index → raw observation count. */\n toCounts: Map<number, number>;\n}\n\nexport interface SerializedMarkovGraph {\n /** index → state label; '' marks a tombstone (freed) slot */\n states: string[];\n /** sparse rows: [fromIndex, total, [toIndex, count][]] */\n rows: Array<[number, number, Array<[number, number]>]>;\n /** explicit list of freed (tombstoned) slot indices */\n freedIndices: number[];\n}\n\n/**\n * Sparse Markov graph for first-order (and optional second-order bigram) state transitions.\n *\n * **Index stability design:**\n * States are assigned an integer index on first encounter and that index\n * is never re-numbered. This makes the binary codec trivially verifiable\n * (indices in the encoded rows map directly to the states array) and means\n * serialized data stays valid even after LFU pruning, because pruned slots\n * are *tombstoned* (set to `''`) rather than compacted.\n *\n * **Sparse Map representation:**\n * `rows` is a `Map<fromIndex, TransitionRow>` rather than a flat array.\n * Navigation graphs are typically sparse (most state pairs are never\n * observed), so flat N×N matrices would waste memory and inflate encoded size.\n */\nexport class MarkovGraph {\n private readonly rows = new Map<number, TransitionRow>();\n private readonly stateToIndex = new Map<string, number>();\n private readonly indexToState: string[] = [];\n private readonly freedIndices: number[] = [];\n\n readonly highEntropyThreshold: number;\n readonly divergenceThreshold: number;\n readonly smoothingEpsilon: number;\n readonly baselineMeanLL?: number;\n readonly baselineStdLL?: number;\n readonly maxStates: number;\n /**\n * Dirichlet / Laplace smoothing pseudo-count.\n * When > 0, `getProbability` uses:\n * P = (count + alpha) / (total + alpha * k)\n * where k = number of live states. When 0, falls back to exact\n * frequentist math (count / total) with zero extra cost.\n *\n * **Default: `0.1`** — mild Bayesian regularization that prevents\n * cold-start 100 % probability spikes on Day-1 sessions.\n * Pass `0` explicitly to restore pure frequentist behaviour.\n *\n * Non-finite or negative values are clamped to `0` by the constructor.\n */\n readonly smoothingAlpha: number;\n\n constructor(config: MarkovGraphConfig = {}) {\n this.highEntropyThreshold = config.highEntropyThreshold ?? 0.75;\n this.divergenceThreshold = Math.abs(config.divergenceThreshold ?? 3.5);\n this.smoothingEpsilon = config.smoothingEpsilon ?? 0.01;\n this.baselineMeanLL = config.baselineMeanLL;\n this.baselineStdLL = config.baselineStdLL;\n this.maxStates = config.maxStates ?? 500;\n const rawSmoothingAlpha = config.smoothingAlpha ?? 0.1;\n this.smoothingAlpha =\n Number.isFinite(rawSmoothingAlpha) && rawSmoothingAlpha >= 0 ? rawSmoothingAlpha : 0;\n }\n\n /**\n * Return the integer index for a state label, allocating a new slot if\n * needed. Reuses tombstoned (freed) slots from LFU pruning before\n * appending to the end of `indexToState`, keeping the array compact.\n *\n * Empty string is rejected because `''` is the internal tombstone marker.\n *\n * ⚠ **Multi-call safety:** `ensureState` is NOT safe to call twice in sequence\n * without first ensuring no prune can fire between the two calls. If a caller\n * resolves index A via `ensureState(a)`, then a prune fires before `ensureState(b)`,\n * state A may be tombstoned (it was just allocated so its `total = 0`, making it the\n * first LFU eviction candidate). Any subsequent write using index A then lands on\n * a dead slot — a \"ghost row\". Any method that calls `ensureState` more than once\n * MUST execute the prune guard BEFORE the first call, never between calls.\n * See `incrementTransition` for the canonical pattern.\n */\n ensureState(state: string): number {\n if (state === '') throw new Error('MarkovGraph: state label must not be empty string');\n const existing = this.stateToIndex.get(state);\n if (existing !== undefined) return existing;\n\n let index: number;\n if (this.freedIndices.length > 0) {\n index = this.freedIndices.pop()!;\n this.indexToState[index] = state;\n } else {\n index = this.indexToState.length;\n this.indexToState.push(state);\n }\n this.stateToIndex.set(state, index);\n return index;\n }\n\n incrementTransition(fromState: string, toState: string): void {\n // Burst-bloat guard: prune BEFORE resolving any indices so that neither\n // fromState nor toState can be tombstoned mid-resolution. Placing this\n // inside ensureState caused a ghost-row bug: a freshly-allocated fromState\n // index (total=0) would be evicted by the prune triggered during the\n // subsequent ensureState(toState) call, leaving a phantom row at a\n // tombstoned index with no stateToIndex entry.\n if (this.stateToIndex.size >= this.maxStates * 1.5) {\n this.prune();\n }\n const from = this.ensureState(fromState);\n const to = this.ensureState(toState);\n\n const row = this.rows.get(from) ?? { total: 0, toCounts: new Map<number, number>() };\n const nextCount = (row.toCounts.get(to) ?? 0) + 1;\n row.toCounts.set(to, nextCount);\n row.total += 1;\n this.rows.set(from, row);\n }\n\n /**\n * P(to|from) from live counts, with optional Bayesian Laplace smoothing.\n *\n * When `smoothingAlpha > 0`:\n * P = (count + α) / (total + α × k)\n * where k = live-state count (`stateToIndex.size`).\n * α = 0 falls back to exact frequentist math with no overhead.\n *\n * No allocations are made during this call.\n */\n getProbability(fromState: string, toState: string): number {\n const from = this.stateToIndex.get(fromState);\n const to = this.stateToIndex.get(toState);\n if (from === undefined || to === undefined) return 0;\n\n const row = this.rows.get(from);\n if (!row || row.total === 0) return 0;\n\n const count = row.toCounts.get(to) ?? 0;\n if (this.smoothingAlpha === 0) {\n return count / row.total;\n }\n return (\n (count + this.smoothingAlpha) / (row.total + this.smoothingAlpha * this.stateToIndex.size)\n );\n }\n\n /**\n * Entropy H(i) = -Σ P(i->j) log P(i->j)\n * Returned entropy is in nats (natural log).\n *\n * When `smoothingAlpha > 0`, smoothed probabilities are used for ALL k\n * states (observed + unobserved). The contribution from the\n * `(k - observed)` unobserved transitions is computed analytically —\n * no temporary arrays are allocated.\n */\n entropyForState(state: string): number {\n const from = this.stateToIndex.get(state);\n if (from === undefined) return 0;\n\n const row = this.rows.get(from);\n if (!row || row.total === 0) return 0;\n\n let entropy = 0;\n if (this.smoothingAlpha === 0) {\n row.toCounts.forEach((count) => {\n const p = count / row.total;\n if (p > 0) entropy -= p * Math.log(p);\n });\n } else {\n const k = this.stateToIndex.size;\n const denominator = row.total + this.smoothingAlpha * k;\n\n // Observed transitions\n row.toCounts.forEach((count) => {\n const p = (count + this.smoothingAlpha) / denominator;\n entropy -= p * Math.log(p);\n });\n\n // Unobserved transitions — computed analytically to avoid allocation\n const numUnobserved = k - row.toCounts.size;\n if (numUnobserved > 0) {\n const pUnobserved = this.smoothingAlpha / denominator;\n if (pUnobserved > 0) {\n entropy -= numUnobserved * pUnobserved * Math.log(pUnobserved);\n }\n }\n }\n return entropy;\n }\n\n /**\n * Normalized entropy in [0..1], dividing by max entropy ln(k)\n * where k is the **local** branching factor (number of observed outgoing\n * edges from this state, floored at 2 to avoid division artifacts).\n *\n * Using the local fan-out instead of the global state count ensures that\n * the normalized score correctly reflects *local* decision-space confusion\n * (rage-clicking between 3–4 links) regardless of how many total pages\n * exist in the graph. With the global denominator, ln(N) grows without\n * bound as users browse more pages, crushing the normalized score and\n * making high-entropy anomalies undetectable after ~10 unique states.\n *\n * **Smoothing note:** this method uses raw frequentist counts\n * (count / row.total) for both the entropy numerator and the ln(supportSize)\n * denominator so that both are anchored to the same local support.\n * `entropyForState()` uses Bayesian Laplace smoothing over all k global\n * states and is intentionally a different (larger) quantity — calling it\n * here would make the numerator exceed the denominator whenever the graph\n * has many states, producing raw normalized scores > 1 that the clamp would\n * silently mask.\n */\n normalizedEntropyForState(state: string): number {\n const from = this.stateToIndex.get(state);\n if (from === undefined) return 0;\n\n const row = this.rows.get(from);\n if (!row || row.total === 0) return 0;\n\n const supportSize = Math.max(2, row.toCounts.size);\n const maxEntropy = Math.log(supportSize);\n if (maxEntropy <= 0) return 0;\n\n // Compute entropy using only the observed local transitions (frequentist).\n // This keeps the numerator and denominator anchored to the same support\n // (local fan-out), avoiding the mismatch that would arise from calling\n // entropyForState() which spreads probability mass over all k global states\n // in Bayesian mode.\n let entropy = 0;\n row.toCounts.forEach((count) => {\n const p = count / row.total;\n if (p > 0) entropy -= p * Math.log(p);\n });\n\n // ── Invariant: entropy ≤ maxEntropy (should hold WITHOUT the clamp) ────────\n // By the maximum-entropy principle, H(p) = -Σ p_i ln(p_i) ≤ ln(k) for any\n // probability distribution over k outcomes. Here k = supportSize and\n // maxEntropy = ln(supportSize), so entropy / maxEntropy ∈ [0, 1] by construction.\n //\n // Math.min(1, ...) below is a SAFETY NET only — it must never be the\n // mechanism that produces a value ≤ 1. If it fires, the numerator and\n // denominator are using different distributions (regression indicator).\n // Property-based tests in unit-fast.test.mjs assert this invariant directly.\n const normalized = entropy / maxEntropy;\n return Math.min(1, Math.max(0, normalized));\n }\n\n /**\n * Log-likelihood trajectory:\n * log L = Σ log P_baseline(s_t+1 | s_t)\n *\n * To avoid -Infinity when a transition doesn't exist in baseline,\n * apply epsilon smoothing.\n */\n static logLikelihoodTrajectory(\n baseline: MarkovGraph,\n sequence: readonly string[],\n epsilon = 0.01,\n ): number {\n if (sequence.length < 2) return 0;\n\n let sum = 0;\n for (let i = 0; i < sequence.length - 1; i += 1) {\n const p = baseline.getProbability(sequence[i], sequence[i + 1]);\n sum += Math.log(p > 0 ? p : epsilon);\n }\n return sum;\n }\n\n /**\n * Returns all outgoing edges from `fromState` whose transition probability\n * meets or exceeds `minProbability`, sorted descending by probability.\n *\n * Intended for **read-only** UI prefetching hints. The returned state\n * labels are raw values from the internal transition graph and may include\n * sensitive routes.\n *\n * ⚠ **Security notice — you MUST filter results before acting on them.**\n * Always pass a `sanitize` predicate (see `IntentManager.predictNextStates`)\n * that rejects state-mutating or privacy-sensitive routes such as\n * `/logout`, `/checkout/pay`, or any route containing PII.\n * Prefetching must **never** trigger state-mutating side effects.\n *\n * @param fromState The source state to query outgoing transitions from.\n * @param minProbability Minimum probability threshold in [0, 1] (inclusive).\n * Values ≤ 0 return all edges; values > 1 return none.\n * @returns Array of `{ state, probability }` objects, sorted by probability\n * descending. Returns an empty array when the state is unknown or\n * has no recorded transitions.\n */\n getLikelyNextStates(\n fromState: string,\n minProbability: number,\n ): { state: string; probability: number }[] {\n const fromIndex = this.stateToIndex.get(fromState);\n if (fromIndex === undefined) return [];\n\n const row = this.rows.get(fromIndex);\n if (!row || row.total === 0) return [];\n\n const results: { state: string; probability: number }[] = [];\n // Pre-compute smoothing denominator once — O(1), no allocation.\n const denominator =\n this.smoothingAlpha === 0\n ? row.total\n : row.total + this.smoothingAlpha * this.stateToIndex.size;\n\n row.toCounts.forEach((count, toIndex) => {\n const probability =\n this.smoothingAlpha === 0\n ? count / denominator\n : (count + this.smoothingAlpha) / denominator;\n if (probability >= minProbability) {\n const label = this.indexToState[toIndex];\n if (label && label !== '') {\n results.push({ state: label, probability });\n }\n }\n });\n\n results.sort((a, b) => b.probability - a.probability);\n return results;\n }\n\n /**\n * Returns the total number of outgoing transitions recorded for a state.\n * Used as a minimum-sample guard before firing entropy/divergence events.\n */\n rowTotal(state: string): number {\n const from = this.stateToIndex.get(state);\n if (from === undefined) return 0;\n return this.rows.get(from)?.total ?? 0;\n }\n\n /**\n * Total number of allocated index slots, including tombstoned (freed) ones.\n *\n * ⚠ This is NOT the count of live states. Use `stateToIndex.size`\n * (via `prune` or `fromJSON`) when you need the live count.\n * The value is useful for sizing serialization buffers.\n */\n stateCount(): number {\n return this.indexToState.length;\n }\n\n totalTransitions(): number {\n let total = 0;\n this.rows.forEach((row) => {\n total += row.total;\n });\n return total;\n }\n\n /**\n * LFU (Least-Frequently-Used) pruning.\n *\n * When the number of live states exceeds `maxStates`, evict the bottom\n * ~20 % of states ranked by total outgoing transitions. Rather than\n * re-indexing (which would invalidate every edge reference), pruned\n * states are \"tombstoned\":\n *\n * 1. Their outgoing row is deleted from `this.rows`.\n * 2. Any inbound edges referencing them are removed from other rows.\n * 3. The slot in `indexToState` is set to '' (dead index) and the\n * entry is removed from `stateToIndex`.\n *\n * This is O(S + E) where S = stateCount and E = total edge entries.\n */\n prune(): void {\n const liveCount = this.stateToIndex.size;\n if (liveCount <= this.maxStates) return;\n\n // ── 1. Rank every live state by total outgoing transitions (LFU) ──\n // Map.forEach callback: (value: number, key: string) where value = index.\n const ranked: Array<{ index: number; total: number }> = [];\n this.stateToIndex.forEach((idx) => {\n const row = this.rows.get(idx);\n ranked.push({ index: idx, total: row?.total ?? 0 });\n });\n\n // Sort ascending by total transitions (lowest first = least used).\n ranked.sort((a, b) => a.total - b.total);\n\n // Evict bottom 20 % (at least 1, at most enough to get back to maxStates).\n const evictTarget = Math.max(\n 1,\n Math.min(Math.ceil(liveCount * 0.2), liveCount - this.maxStates),\n );\n const evictSet = new Set<number>();\n for (let i = 0; i < evictTarget && i < ranked.length; i += 1) {\n evictSet.add(ranked[i].index);\n }\n\n // ── 2. Remove outgoing rows for evicted states ──\n evictSet.forEach((idx) => {\n this.rows.delete(idx);\n });\n\n // ── 3. Scrub inbound edges from surviving rows ──\n this.rows.forEach((row) => {\n let removedTotal = 0;\n evictSet.forEach((deadIdx) => {\n const count = row.toCounts.get(deadIdx);\n if (count !== undefined) {\n removedTotal += count;\n row.toCounts.delete(deadIdx);\n }\n });\n row.total -= removedTotal;\n });\n\n // ── 4. Tombstone index slots and register for reuse ──\n evictSet.forEach((idx) => {\n const label = this.indexToState[idx];\n if (label !== undefined && label !== '') {\n this.stateToIndex.delete(label);\n }\n this.freedIndices.push(idx);\n this.indexToState[idx] = ''; // dead slot\n });\n }\n\n toJSON(): SerializedMarkovGraph {\n const rows: SerializedMarkovGraph['rows'] = [];\n this.rows.forEach((row, fromIndex) => {\n const edges: Array<[number, number]> = [];\n row.toCounts.forEach((count, toIndex) => {\n edges.push([toIndex, count]);\n });\n rows.push([fromIndex, row.total, edges]);\n });\n\n return {\n states: [...this.indexToState],\n rows,\n freedIndices: [...this.freedIndices],\n };\n }\n\n static fromJSON(data: SerializedMarkovGraph, config: MarkovGraphConfig = {}): MarkovGraph {\n const graph = new MarkovGraph(config);\n const tombstoneSet = new Set(data.freedIndices);\n\n for (let i = 0; i < data.states.length; i += 1) {\n const label = data.states[i];\n graph.indexToState.push(label);\n\n if (tombstoneSet.has(i)) {\n if (label !== '') {\n throw new Error(\n `MarkovGraph.fromJSON: slot ${i} is listed in freedIndices but has non-empty label \"${label}\"`,\n );\n }\n graph.freedIndices.push(i);\n } else {\n if (label === '') {\n throw new Error(\n `MarkovGraph.fromJSON: slot ${i} has an empty-string label but is not listed in ` +\n `freedIndices. Empty string is reserved as the tombstone marker.`,\n );\n }\n graph.stateToIndex.set(label, i);\n }\n }\n\n for (let r = 0; r < data.rows.length; r += 1) {\n const [fromIndex, total, edges] = data.rows[r];\n const row: TransitionRow = { total, toCounts: new Map<number, number>() };\n for (let e = 0; e < edges.length; e += 1) {\n const [toIndex, count] = edges[e];\n row.toCounts.set(toIndex, count);\n }\n graph.rows.set(fromIndex, row);\n }\n\n return graph;\n }\n\n /* ================================================================== */\n /* Binary serialization — zero-dependency, zero JSON.stringify */\n /* ================================================================== */\n\n /**\n * Binary wire format — version 0x02, little-endian throughout:\n *\n * ┌──────────────────────────────────────┐\n * │ Version : Uint8 (1B) │ — always 0x02\n * │ NumStates : Uint16 (2B) │\n * │ ┌── for each state ───────────────┐ │\n * │ │ StringByteLen : Uint16 (2B) │ │ — 0 bytes for tombstone slots\n * │ │ UTF-8 Bytes : [N] │ │\n * │ └─────────────────────────────────┘ │\n * │ NumFreedIndices : Uint16 (2B) │ — 0 if no tombstones\n * │ ┌── for each freed index ─────────┐ │\n * │ │ SlotIndex : Uint16 (2B) │ │\n * │ └─────────────────────────────────┘ │\n * │ NumRows : Uint16 (2B) │\n * │ ┌── for each row ──────────────────┐ │\n * │ │ FromIndex : Uint16 (2B) │ │\n * │ │ Total : Uint32 (4B) │ │\n * │ │ NumEdges : Uint16 (2B) │ │\n * │ │ ┌── for each edge ─────────────┐ │ │\n * │ │ │ ToIndex : Uint16 (2B) │ │ │\n * │ │ │ Count : Uint32 (4B) │ │ │\n * │ │ └──────────────────────────────┘ │ │\n * │ └──────────────────────────────────┘ │\n * └──────────────────────────────────────┘\n */\n toBinary(): Uint8Array {\n const encoder = new TextEncoder();\n\n // ── Pre-compute total buffer size so we allocate exactly once ──\n\n // Header: version (1B) + numStates (2B) = 3 bytes\n let totalSize = 3;\n\n // Encode all state labels to UTF-8 up front and cache the buffers.\n const encodedLabels: Uint8Array[] = new Array(this.indexToState.length);\n for (let i = 0; i < this.indexToState.length; i += 1) {\n encodedLabels[i] = encoder.encode(this.indexToState[i]);\n // Per state: stringByteLen (Uint16 = 2B) + actual bytes\n totalSize += 2 + encodedLabels[i].byteLength;\n }\n\n // V2 freed-index section: numFreedIndices (2B) + freed index values (2B each)\n totalSize += 2 + this.freedIndices.length * 2;\n\n // NumRows header: 2 bytes\n totalSize += 2;\n\n // Per row: fromIndex (2B) + total (4B) + numEdges (2B) = 8 bytes\n // Per edge: toIndex (2B) + count (4B) = 6 bytes\n this.rows.forEach((row) => {\n totalSize += 8; // row header\n totalSize += row.toCounts.size * 6; // edges\n });\n\n // ── Allocate buffer + DataView ──\n const buffer = new Uint8Array(totalSize);\n const view = new DataView(buffer.buffer);\n let offset = 0;\n\n // ── Write header ──\n\n // Byte 0: format version (0x02 — adds explicit freed-index list)\n view.setUint8(offset, 0x02); // version = 2\n offset += 1; // offset now 1\n\n // Bytes 1-2: number of states (Uint16 LE)\n view.setUint16(offset, this.indexToState.length, true);\n offset += 2; // offset now 3\n\n // ── Write state labels ──\n for (let i = 0; i < this.indexToState.length; i += 1) {\n const encoded = encodedLabels[i];\n\n // 2 bytes: UTF-8 byte length of this label\n view.setUint16(offset, encoded.byteLength, true);\n offset += 2;\n\n // N bytes: raw UTF-8 payload\n buffer.set(encoded, offset);\n offset += encoded.byteLength;\n }\n\n // ── Write freed-index list (V2) ──\n\n // 2 bytes: number of freed indices (Uint16 LE)\n view.setUint16(offset, this.freedIndices.length, true);\n offset += 2;\n\n // 2 bytes each: freed slot index (Uint16 LE)\n for (let i = 0; i < this.freedIndices.length; i += 1) {\n view.setUint16(offset, this.freedIndices[i], true);\n offset += 2;\n }\n\n // ── Write rows header ──\n\n // 2 bytes: number of rows with data\n view.setUint16(offset, this.rows.size, true);\n offset += 2;\n\n // ── Write each row ──\n this.rows.forEach((row, fromIndex) => {\n // 2 bytes: fromIndex (Uint16 LE)\n view.setUint16(offset, fromIndex, true);\n offset += 2;\n\n // 4 bytes: total outgoing transitions (Uint32 LE)\n view.setUint32(offset, row.total, true);\n offset += 4;\n\n // 2 bytes: number of edges (Uint16 LE)\n view.setUint16(offset, row.toCounts.size, true);\n offset += 2;\n\n // Per edge: toIndex (2B) + count (4B)\n row.toCounts.forEach((count, toIndex) => {\n // 2 bytes: destination state index (Uint16 LE)\n view.setUint16(offset, toIndex, true);\n offset += 2;\n\n // 4 bytes: transition count (Uint32 LE)\n view.setUint32(offset, count, true);\n offset += 4;\n });\n });\n\n return buffer;\n }\n\n /**\n * Reconstruct a MarkovGraph from the binary format produced by `toBinary()`.\n * See the encoding spec in `toBinary()` for the wire layout.\n */\n static fromBinary(buffer: Uint8Array, config: MarkovGraphConfig = {}): MarkovGraph {\n const graph = new MarkovGraph(config);\n const decoder = new TextDecoder();\n const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);\n let offset = 0;\n\n // ── Read header ──\n\n // Byte 0: version — only 0x02 is supported.\n const version = view.getUint8(offset);\n offset += 1;\n if (version !== 0x02) {\n throw new Error(\n `Unsupported MarkovGraph binary version: 0x${version.toString(16).padStart(2, '0')}. ` +\n `Only version 0x02 is supported.`,\n );\n }\n\n // Bytes 1-2: number of states (Uint16 LE)\n const numStates = view.getUint16(offset, true);\n offset += 2;\n\n // ── Read state labels into a temporary buffer ──\n // Classification (live vs tombstone) happens after reading the freed-index\n // list, so we store raw labels first and populate the graph below.\n const rawLabels: string[] = [];\n for (let i = 0; i < numStates; i += 1) {\n const strLen = view.getUint16(offset, true);\n offset += 2;\n rawLabels.push(decoder.decode(buffer.subarray(offset, offset + strLen)));\n offset += strLen;\n }\n\n // ── Read freed-index list and classify slots ──\n const numFreed = view.getUint16(offset, true);\n offset += 2;\n\n const tombstoneSet = new Set<number>();\n for (let i = 0; i < numFreed; i += 1) {\n tombstoneSet.add(view.getUint16(offset, true));\n offset += 2;\n }\n\n for (let i = 0; i < rawLabels.length; i += 1) {\n const label = rawLabels[i];\n graph.indexToState.push(label);\n if (tombstoneSet.has(i)) {\n if (label !== '') {\n throw new Error(\n `MarkovGraph.fromBinary: slot ${i} is listed as freed but has non-empty label \"${label}\"`,\n );\n }\n graph.freedIndices.push(i);\n } else {\n if (label === '') {\n throw new Error(\n `MarkovGraph.fromBinary: slot ${i} has an empty-string label but is not listed ` +\n `in the freed-index section. Empty string is reserved as the tombstone marker.`,\n );\n }\n graph.stateToIndex.set(label, i);\n }\n }\n\n // ── Read rows ──\n\n // 2 bytes: number of rows\n const numRows = view.getUint16(offset, true);\n offset += 2;\n\n for (let r = 0; r < numRows; r += 1) {\n // 2 bytes: fromIndex\n const fromIndex = view.getUint16(offset, true);\n offset += 2;\n\n // 4 bytes: total transitions\n const total = view.getUint32(offset, true);\n offset += 4;\n\n // 2 bytes: number of edges\n const numEdges = view.getUint16(offset, true);\n offset += 2;\n\n const toCounts = new Map<number, number>();\n for (let e = 0; e < numEdges; e += 1) {\n // 2 bytes: toIndex\n const toIndex = view.getUint16(offset, true);\n offset += 2;\n\n // 4 bytes: count\n const count = view.getUint32(offset, true);\n offset += 4;\n\n toCounts.set(toIndex, count);\n }\n\n graph.rows.set(fromIndex, { total, toCounts });\n }\n\n return graph;\n }\n}\n\n/**\n * Smoothing epsilon for log-likelihood calculations.\n * Must be identical between calibration and runtime.\n */\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nexport interface BenchmarkConfig {\n enabled?: boolean;\n maxSamples?: number;\n}\n\nexport interface OperationStats {\n count: number;\n avgMs: number;\n p95Ms: number;\n p99Ms: number;\n maxMs: number;\n}\n\nexport interface MemoryFootprintReport {\n stateCount: number;\n totalTransitions: number;\n bloomBitsetBytes: number;\n serializedGraphBytes: number;\n}\n\nexport interface PerformanceReport {\n benchmarkEnabled: boolean;\n track: OperationStats;\n bloomAdd: OperationStats;\n bloomCheck: OperationStats;\n incrementTransition: OperationStats;\n entropyComputation: OperationStats;\n divergenceComputation: OperationStats;\n memoryFootprint: MemoryFootprintReport;\n}\n\n/**\n * Circular-buffer reservoir for per-operation timing samples.\n *\n * Once `maxSamples` is reached, new samples overwrite old ones at position\n * `count % maxSamples` (not true reservoir / random sampling). This means\n * percentile estimates (p95, p99) are computed over the *most-recent*\n * `maxSamples` observations rather than a statistically unbiased sample of\n * all observations. For the SDK’s benchmark use-case (detecting regressions\n * in the steady-state hot path) recency bias is acceptable. If you need\n * unbiased percentiles across an arbitrary run duration, increase `maxSamples`\n * in the `BenchmarkConfig` to cover the full session.\n */\ninterface BenchmarkAccumulator {\n count: number;\n totalMs: number;\n maxMs: number;\n samples: number[];\n}\n\nconst DEFAULT_BENCHMARK_MAX_SAMPLES = 4096;\n\nfunction createAccumulator(): BenchmarkAccumulator {\n return { count: 0, totalMs: 0, maxMs: 0, samples: [] };\n}\n\nfunction recordSample(acc: BenchmarkAccumulator, elapsedMs: number, maxSamples: number): void {\n acc.count += 1;\n acc.totalMs += elapsedMs;\n if (elapsedMs > acc.maxMs) acc.maxMs = elapsedMs;\n if (acc.samples.length < maxSamples) {\n acc.samples.push(elapsedMs);\n } else if (maxSamples > 0) {\n acc.samples[acc.count % maxSamples] = elapsedMs;\n }\n}\n\nfunction percentile(sorted: number[], p: number): number {\n if (sorted.length === 0) return 0;\n const idx = Math.max(0, Math.min(sorted.length - 1, Math.ceil(p * sorted.length) - 1));\n return sorted[idx];\n}\n\nfunction toOperationStats(acc: BenchmarkAccumulator): OperationStats {\n if (acc.count === 0) return { count: 0, avgMs: 0, p95Ms: 0, p99Ms: 0, maxMs: 0 };\n\n const sorted = [...acc.samples].sort((a, b) => a - b);\n return {\n count: acc.count,\n avgMs: acc.totalMs / acc.count,\n p95Ms: percentile(sorted, 0.95),\n p99Ms: percentile(sorted, 0.99),\n maxMs: acc.maxMs,\n };\n}\n\nconst textEncoder = typeof TextEncoder !== 'undefined' ? new TextEncoder() : null;\n\ntype OpName =\n | 'track'\n | 'bloomAdd'\n | 'bloomCheck'\n | 'incrementTransition'\n | 'entropyComputation'\n | 'divergenceComputation';\n\n/**\n * Optional performance recorder for the SDK’s internal hot-path operations.\n *\n * Disabled by default (`enabled: false`) — all `now()` and `record()` calls\n * are no-ops when disabled, so there is zero overhead in production unless\n * callers explicitly opt in via `IntentManagerConfig.benchmark.enabled: true`.\n *\n * Exposes p95/p99 latency and a memory-footprint snapshot via `report()`.\n */\nexport class BenchmarkRecorder {\n readonly enabled: boolean;\n private readonly maxSamples: number;\n private readonly stats: Record<OpName, BenchmarkAccumulator>;\n\n constructor(config: BenchmarkConfig = {}) {\n this.enabled = config.enabled ?? false;\n this.maxSamples = config.maxSamples ?? DEFAULT_BENCHMARK_MAX_SAMPLES;\n this.stats = {\n track: createAccumulator(),\n bloomAdd: createAccumulator(),\n bloomCheck: createAccumulator(),\n incrementTransition: createAccumulator(),\n entropyComputation: createAccumulator(),\n divergenceComputation: createAccumulator(),\n };\n }\n\n now(): number {\n return this.enabled ? performance.now() : 0;\n }\n\n record(operation: OpName, startedAt: number): void {\n if (!this.enabled) return;\n recordSample(this.stats[operation], performance.now() - startedAt, this.maxSamples);\n }\n\n report(memoryFootprint: MemoryFootprintReport): PerformanceReport {\n return {\n benchmarkEnabled: this.enabled,\n track: toOperationStats(this.stats.track),\n bloomAdd: toOperationStats(this.stats.bloomAdd),\n bloomCheck: toOperationStats(this.stats.bloomCheck),\n incrementTransition: toOperationStats(this.stats.incrementTransition),\n entropyComputation: toOperationStats(this.stats.entropyComputation),\n divergenceComputation: toOperationStats(this.stats.divergenceComputation),\n memoryFootprint,\n };\n }\n\n serializedSizeBytes(payload: unknown): number {\n const asText = JSON.stringify(payload);\n return textEncoder ? textEncoder.encode(asText).byteLength : asText.length;\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/**\n * Isomorphic adapters for storage and timers.\n * --------------------------------------------------------\n * Allows the SDK to run safely in SSR environments (Next.js,\n * Nuxt, Remix, etc.) where `window` / `localStorage` are\n * not available at import time or at runtime.\n *\n * Browser implementations gracefully degrade to no-ops when\n * the DOM globals are absent.\n */\n\n/* ------------------------------------------------------------------ */\n/* Storage Adapter */\n/* ------------------------------------------------------------------ */\n\nexport interface StorageAdapter {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n}\n\n/**\n * Async storage adapter for environments where storage I/O is inherently\n * asynchronous (React Native AsyncStorage, Capacitor Preferences, IndexedDB\n * wrappers, etc.).\n *\n * Use `IntentManager.createAsync(config)` to initialize the engine with an\n * async backend — the factory awaits the initial `getItem` call before\n * constructing the engine, preserving the synchronous hot-path for `track()`.\n *\n * The synchronous `StorageAdapter` interface remains the default for\n * browser `localStorage`-backed use cases.\n */\nexport interface AsyncStorageAdapter {\n getItem(key: string): Promise<string | null>;\n setItem(key: string, value: string): Promise<void>;\n}\n\n/**\n * localStorage-backed adapter.\n * Falls back to no-ops when `window` or `localStorage` is unavailable\n * (e.g. SSR, Web Workers, or restrictive iframes).\n */\nexport class BrowserStorageAdapter implements StorageAdapter {\n getItem(key: string): string | null {\n if (typeof window === 'undefined' || !window.localStorage) return null;\n try {\n return window.localStorage.getItem(key);\n } catch {\n // SecurityError in sandboxed iframes / opaque origins\n return null;\n }\n }\n\n setItem(key: string, value: string): void {\n if (typeof window === 'undefined' || !window.localStorage) return;\n // QuotaExceededError / SecurityError are intentionally NOT caught here.\n // The caller (IntentManager.persist) wraps this in its own try/catch\n // so the error surfaces through the configured onError callback.\n window.localStorage.setItem(key, value);\n }\n}\n\n/* ------------------------------------------------------------------ */\n/* Timer Adapter */\n/* ------------------------------------------------------------------ */\n\n/**\n * Opaque handle returned by the timer adapter.\n * Bridges the gap between browser (number) and Node.js (Timeout) return types.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type TimerHandle = any;\n\nexport interface TimerAdapter {\n setTimeout(fn: () => void, delay: number): TimerHandle;\n clearTimeout(id: TimerHandle): void;\n now(): number;\n}\n\n/**\n * Timer adapter backed by the global `setTimeout` / `clearTimeout`.\n * Uses `globalThis` so it works in browsers, Node.js, Deno, Bun, and\n * Cloudflare Workers alike.\n */\nexport class BrowserTimerAdapter implements TimerAdapter {\n setTimeout(fn: () => void, delay: number): TimerHandle {\n if (typeof globalThis.setTimeout !== 'function') {\n // Edge-case: extremely minimal JS runtimes without timers.\n return 0 as TimerHandle;\n }\n return globalThis.setTimeout(fn, delay);\n }\n\n clearTimeout(id: TimerHandle): void {\n if (typeof globalThis.clearTimeout !== 'function') return;\n globalThis.clearTimeout(id);\n }\n\n now(): number {\n if (\n typeof globalThis.performance !== 'undefined' &&\n typeof globalThis.performance.now === 'function'\n ) {\n return globalThis.performance.now();\n }\n return Date.now();\n }\n}\n\n/* ------------------------------------------------------------------ */\n/* In-Memory Adapter (useful for tests / SSR) */\n/* ------------------------------------------------------------------ */\n\n/**\n * Simple in-memory storage adapter.\n * Handy for unit tests and server-side rendering where persistence is\n * neither needed nor available.\n */\nexport class MemoryStorageAdapter implements StorageAdapter {\n private readonly store = new Map<string, string>();\n\n getItem(key: string): string | null {\n return this.store.get(key) ?? null;\n }\n\n setItem(key: string, value: string): void {\n this.store.set(key, value);\n }\n}\n\n/* ------------------------------------------------------------------ */\n/* Lifecycle Adapter */\n/* ------------------------------------------------------------------ */\n\n/**\n * Abstracts browser DOM lifecycle events (page visibility) out of the core\n * engine so that the SDK can be used safely in React Native, Electron, and\n * server-side / SSR environments where `document` is absent.\n *\n * Implementations should call registered callbacks when the host environment\n * transitions between an active (\"resumed\") and an inactive (\"paused\") state.\n */\nexport interface LifecycleAdapter {\n /**\n * Register a callback to be invoked when the environment becomes inactive.\n * Returns an unsubscribe function that removes only this callback, leaving\n * any other registered callbacks untouched.\n */\n onPause(callback: () => void): () => void;\n /**\n * Register a callback to be invoked when the environment becomes active.\n * Returns an unsubscribe function that removes only this callback, leaving\n * any other registered callbacks untouched.\n */\n onResume(callback: () => void): () => void;\n /**\n * Optional: register a callback to be invoked on any user interaction\n * (mouse, keyboard, scroll, touch). Used by the idle-state detector.\n *\n * Implementations should throttle the callback internally (e.g. max once\n * per 1 000 ms) to avoid flooding the engine with high-frequency events.\n *\n * Returns an unsubscribe function that removes only this callback, or\n * `null` when the environment cannot deliver interaction events (e.g.\n * SSR, Node.js tests with a stubbed `window`).\n *\n * Backward-compatible — adapters that do not implement this method are\n * silently skipped and idle detection is disabled.\n */\n onInteraction?(callback: () => void): (() => void) | null;\n /**\n * Optional: register a callback to be invoked when the user signals exit\n * intent by moving the pointer above the viewport top edge (toward the\n * browser chrome / address bar).\n *\n * Implementations should only fire the callback when `MouseEvent.clientY <= 0`\n * so that normal in-page mouse movement is ignored.\n *\n * Returns an unsubscribe function that removes only this callback.\n *\n * Backward-compatible — adapters that do not implement this method are\n * silently skipped and exit-intent detection is disabled.\n */\n onExitIntent?(callback: () => void): () => void;\n /** Remove all event listeners and release resources held by this adapter. */\n destroy(): void;\n}\n\n/**\n * Lifecycle adapter backed by the Page Visibility API\n * (`document.visibilitychange`).\n *\n * Guards every `document` access with a `typeof document !== 'undefined'`\n * check so the class can be imported in SSR / Node.js / React Native\n * environments without throwing.\n *\n * Usage:\n * ```ts\n * const lifecycle = new BrowserLifecycleAdapter();\n * lifecycle.onPause(() => {\n * // e.g. flush pending work or persist state\n * });\n * lifecycle.onResume(() => {\n * // e.g. restart timers or resume work\n * });\n * // later…\n * lifecycle.destroy();\n * ```\n */\nexport class BrowserLifecycleAdapter implements LifecycleAdapter {\n private readonly pauseCallbacks: Array<() => void> = [];\n private readonly resumeCallbacks: Array<() => void> = [];\n private readonly interactionCallbacks: Array<() => void> = [];\n private readonly exitIntentCallbacks: Array<() => void> = [];\n private readonly handler: () => void;\n\n /** Tracks the DOM listeners registered for interaction throttling. */\n private interactionHandler: (() => void) | null = null;\n private interactionLastFired = 0;\n private static readonly INTERACTION_THROTTLE_MS = 1_000;\n private static readonly INTERACTION_EVENTS: ReadonlyArray<string> = [\n 'mousemove',\n 'scroll',\n 'touchstart',\n 'keydown',\n ];\n\n /** Handler for exit-intent mouseleave detection on document.documentElement. */\n private exitIntentHandler: ((e: MouseEvent) => void) | null = null;\n\n constructor() {\n this.handler = () => {\n if (typeof document === 'undefined') return;\n if (document.hidden) {\n for (const cb of this.pauseCallbacks) cb();\n } else {\n for (const cb of this.resumeCallbacks) cb();\n }\n };\n\n if (typeof document !== 'undefined') {\n document.addEventListener('visibilitychange', this.handler);\n }\n }\n\n onPause(callback: () => void): () => void {\n this.pauseCallbacks.push(callback);\n return () => {\n const idx = this.pauseCallbacks.indexOf(callback);\n if (idx !== -1) this.pauseCallbacks.splice(idx, 1);\n };\n }\n\n onResume(callback: () => void): () => void {\n this.resumeCallbacks.push(callback);\n return () => {\n const idx = this.resumeCallbacks.indexOf(callback);\n if (idx !== -1) this.resumeCallbacks.splice(idx, 1);\n };\n }\n\n onInteraction(callback: () => void): (() => void) | null {\n // When DOM event APIs are unavailable (SSR, Node.js tests with a\n // stubbed `window`), interaction tracking cannot function.\n // Return null to signal \"not supported\" so the coordinator skips\n // idle-check timer scheduling.\n if (typeof window === 'undefined' || typeof window.addEventListener !== 'function') {\n return null;\n }\n\n this.interactionCallbacks.push(callback);\n\n // Lazily attach DOM listeners on the first subscription.\n if (\n this.interactionHandler === null &&\n typeof window !== 'undefined' &&\n typeof window.addEventListener === 'function'\n ) {\n this.interactionHandler = () => {\n const now =\n typeof performance !== 'undefined' && typeof performance.now === 'function'\n ? performance.now()\n : Date.now();\n if (now - this.interactionLastFired < BrowserLifecycleAdapter.INTERACTION_THROTTLE_MS) {\n return;\n }\n this.interactionLastFired = now;\n for (const cb of this.interactionCallbacks) cb();\n };\n\n const opts: AddEventListenerOptions = { passive: true };\n for (const evt of BrowserLifecycleAdapter.INTERACTION_EVENTS) {\n window.addEventListener(evt, this.interactionHandler, opts);\n }\n }\n\n return () => {\n const idx = this.interactionCallbacks.indexOf(callback);\n if (idx !== -1) this.interactionCallbacks.splice(idx, 1);\n\n // Remove DOM listeners when the last subscriber unsubscribes.\n if (this.interactionCallbacks.length === 0) {\n this.teardownInteractionListeners();\n }\n };\n }\n\n /** Remove interaction DOM listeners if they are currently attached. */\n private teardownInteractionListeners(): void {\n if (\n this.interactionHandler !== null &&\n typeof window !== 'undefined' &&\n typeof window.removeEventListener === 'function'\n ) {\n for (const evt of BrowserLifecycleAdapter.INTERACTION_EVENTS) {\n window.removeEventListener(evt, this.interactionHandler);\n }\n }\n this.interactionHandler = null;\n }\n\n /**\n * Register a callback to be invoked when the user moves the pointer above\n * the top edge of the viewport (clientY <= 0), which typically indicates\n * they are heading toward the browser chrome / address bar — a reliable\n * proxy for exit intent on desktop.\n *\n * The listener is attached lazily on the first subscription so that the\n * adapter stays tree-shakeable in SSR / Node.js environments.\n */\n onExitIntent(callback: () => void): () => void {\n this.exitIntentCallbacks.push(callback);\n\n // Lazily attach the DOM listener on the first subscription.\n if (this.exitIntentHandler === null) {\n const root = typeof document !== 'undefined' ? document?.documentElement : null;\n if (root !== null && root !== undefined) {\n this.exitIntentHandler = (e: MouseEvent) => {\n if (e.clientY <= 0) {\n for (const cb of this.exitIntentCallbacks) cb();\n }\n };\n root.addEventListener('mouseleave', this.exitIntentHandler as EventListener);\n }\n }\n\n return () => {\n const idx = this.exitIntentCallbacks.indexOf(callback);\n if (idx !== -1) this.exitIntentCallbacks.splice(idx, 1);\n\n // Remove the DOM listener when the last subscriber unsubscribes.\n if (this.exitIntentCallbacks.length === 0) {\n this.teardownExitIntentListener();\n }\n };\n }\n\n /** Remove the exit-intent DOM listener if currently attached. */\n private teardownExitIntentListener(): void {\n if (this.exitIntentHandler !== null) {\n const root = typeof document !== 'undefined' ? document?.documentElement : null;\n if (root !== null && root !== undefined) {\n root.removeEventListener('mouseleave', this.exitIntentHandler as EventListener);\n }\n }\n this.exitIntentHandler = null;\n }\n\n destroy(): void {\n if (typeof document !== 'undefined') {\n document.removeEventListener('visibilitychange', this.handler);\n }\n this.teardownInteractionListeners();\n this.teardownExitIntentListener();\n this.pauseCallbacks.length = 0;\n this.resumeCallbacks.length = 0;\n this.interactionCallbacks.length = 0;\n this.exitIntentCallbacks.length = 0;\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/**\n * Pre-compiled patterns for dynamic path-segment detection.\n *\n * Both regexes are compiled **once at module load time** so that every\n * `normalizeRouteState()` call is allocation-free on the hot path.\n */\n\n/**\n * Matches a v4 UUID in its canonical 8-4-4-4-12 hex form.\n *\n * Version constraint: the third group starts with `4` (version bit).\n * Variant constraint: the fourth group starts with `8`, `9`, `a`, or `b`\n * (RFC 4122 variant bits).\n * Case-insensitive: both upper- and lower-case hex digits are accepted.\n */\nconst UUID_V4_RE = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/gi;\n\n/**\n * Matches a MongoDB ObjectID: exactly 24 hexadecimal characters forming a\n * complete word (no adjoining word characters on either side).\n *\n * The `\\b` word-boundary anchors prevent partial matches inside longer hex\n * strings (e.g. a 26-char string is not accidentally truncated).\n * Case-insensitive so both lowercase storage and uppercase display variants\n * are handled.\n */\nconst MONGO_ID_RE = /\\b[0-9a-f]{24}\\b/gi;\n\n/**\n * Matches a path segment that is a purely numeric integer ID with 4 or more\n * digits. The threshold of 4 avoids stripping common low-value segments\n * like `/page/2` (pagination) or `/step/3` (wizard steps).\n *\n * Pattern breakdown:\n * `\\/` — must be preceded by a slash (path separator)\n * `\\d{4,}` — four or more consecutive digits\n * `(?=\\/|$)` — followed by another slash or end-of-string (segment boundary)\n *\n * Global flag: multiple numeric segments in one path are all replaced.\n */\nconst NUMERIC_ID_RE = /\\/\\d{4,}(?=\\/|$)/g;\n\n/**\n * Normalizes a URL or route path to a canonical state key for use as an\n * `IntentManager` state label.\n *\n * Transformations applied in order:\n *\n * 1. **Strip query string** — everything from the first `?` onwards is removed.\n * 2. **Strip hash fragment** — everything from the first `#` onwards is removed.\n * 3. **Replace dynamic ID segments** — v4 UUIDs, 24-character hex MongoDB\n * ObjectIDs, and numeric IDs with 4+ digits found anywhere in the remaining\n * path are replaced with `:id`.\n * 4. **Remove trailing slash** — a single trailing `/` is removed so that\n * `/checkout/` and `/checkout` resolve to the same state. The bare root\n * path `/` is left unchanged.\n *\n * The function is **pure**: identical inputs always produce identical outputs\n * and it has no side-effects. All regex patterns are compiled once at module\n * load time so the per-call cost is limited to string scanning.\n *\n * @example\n * normalizeRouteState('/users/550e8400-e29b-41d4-a716-446655440000/profile?tab=bio#section')\n * // → '/users/:id/profile'\n *\n * @example\n * normalizeRouteState('/products/507f1f77bcf86cd799439011/reviews/')\n * // → '/products/:id/reviews'\n *\n * @example\n * normalizeRouteState('/user/12345/profile')\n * // → '/user/:id/profile'\n *\n * @example\n * normalizeRouteState('/checkout/')\n * // → '/checkout'\n *\n * @param url - A URL string or path, e.g. from `window.location.href` or\n * `window.location.pathname`.\n * @returns The normalized, canonical route state string.\n */\nexport function normalizeRouteState(url: string): string {\n // 1. Strip query string — slice at the first '?'\n const qIdx = url.indexOf('?');\n let path = qIdx !== -1 ? url.slice(0, qIdx) : url;\n\n // 2. Strip hash fragment — slice at the first '#'\n const hIdx = path.indexOf('#');\n if (hIdx !== -1) path = path.slice(0, hIdx);\n\n // 3. Replace v4 UUIDs first (more specific), then MongoDB ObjectIDs,\n // then numeric IDs (4+ digit path segments).\n // UUID replacement runs first because a UUID may contain 24-char hex\n // sub-sequences that would otherwise be caught by the ObjectID regex.\n path = path.replace(UUID_V4_RE, ':id').replace(MONGO_ID_RE, ':id').replace(NUMERIC_ID_RE, '/:id');\n\n // 4. Remove a single trailing slash, but preserve the bare root '/'.\n if (path.length > 1 && path.endsWith('/')) {\n path = path.slice(0, -1);\n }\n\n return path;\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/**\n * Laplace smoothing epsilon used for log-likelihood calculations.\n * Must be identical between calibration (intent-sdk-performance) and runtime (IntentManager).\n */\nexport const SMOOTHING_EPSILON = 0.01;\n\n/**\n * Minimum sliding window length before evaluating trajectory.\n * This \"warm-up\" allows the average log-likelihood to stabilize.\n * Must match the warm-up gate in IntentManager.evaluateTrajectory and\n * the calibration sampler in intent-sdk-performance.\n */\nexport const MIN_WINDOW_LENGTH = 16;\n\n/**\n * Maximum sliding window length (recentTrajectory cap).\n * Used as reference for variance scaling.\n * Must match the recentTrajectory cap in IntentManager and the calibration\n * sampler in intent-sdk-performance.\n */\nexport const MAX_WINDOW_LENGTH = 32;\n\n/**\n * Minimum number of outgoing transitions a state must have before entropy\n * evaluation is considered statistically meaningful.\n * Higher values prevent spurious entropy triggers on small samples.\n */\nexport const MIN_SAMPLE_TRANSITIONS = 10;\n\n/**\n * Upper bound on any single dwell-time or hidden-duration measurement (30 minutes).\n *\n * CPU suspend, laptop sleep, and OS hibernation cause the monotonic clock to\n * jump by hours while the Page Visibility API reports the tab as hidden. Any\n * raw delta that exceeds this threshold is almost certainly caused by the host\n * machine being suspended rather than genuine user behaviour and must never be\n * fed into the Welford variance accumulator or used to offset previousStateEnteredAt.\n *\n * When the threshold is breached the engine:\n * 1. Discards the inflated measurement — does NOT update Welford stats.\n * 2. Resets the dwell baseline to the current timestamp so the next\n * measurement starts from a clean epoch.\n * 3. Emits a `session_stale` diagnostic event for host-app observability.\n */\nexport const MAX_PLAUSIBLE_DWELL_MS = 1_800_000; // 30 minutes\n\n/**\n * Minimum tab-hidden duration before the engine considers the user a\n * \"comparison shopper\" and emits an `attention_return` event on resume.\n *\n * 15 seconds is long enough to filter out quick alt-tab / notification\n * glances while capturing users who navigated to a competitor tab.\n */\nexport const ATTENTION_RETURN_THRESHOLD_MS = 15_000; // 15 seconds\n\n/**\n * Duration of user inactivity (no mouse, keyboard, scroll, or touch events)\n * before the engine considers the user idle and emits a `user_idle` event.\n *\n * 2 minutes is a conservative default that avoids false positives from short\n * pauses (reading long content, watching embedded video) while still catching\n * users who genuinely walked away from their device.\n */\nexport const USER_IDLE_THRESHOLD_MS = 120_000; // 2 minutes\n\n/**\n * Interval between successive idle-state checks inside the LifecycleCoordinator.\n *\n * A 5-second polling cadence keeps CPU overhead negligible while ensuring the\n * `user_idle` event fires within 5 seconds of the actual threshold crossing.\n */\nexport const IDLE_CHECK_INTERVAL_MS = 5_000; // 5 seconds\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport type { IntentManagerConfig, MarkovGraphConfig } from '../types/events.js';\nimport { SMOOTHING_EPSILON } from './constants.js';\n\n/**\n * Converts a one-tailed false positive rate to a Z-score threshold via the\n * standard inverse-normal CDF: z = Φ⁻¹(1 − fpr).\n *\n * Uses the rational approximation by Beasley & Springer (1977) — accurate to\n * within 4.5 × 10⁻⁴ across (0, 0.5], which is far tighter than the asymptotic\n * `√(−2 ln fpr)` approximation (≈ 36 % error at fpr = 0.05).\n * Finite inputs outside [0.001, 0.5] are silently clamped.\n * Non-finite inputs (NaN, ±Infinity) return NaN; callers must guard with\n * `Number.isFinite` and fall back to a sensible default.\n *\n * Bundle-size optimised: six scalar constants, no lookup tables.\n */\nfunction fprToZScore(fpr: number): number {\n if (!Number.isFinite(fpr)) return NaN;\n const p = Math.min(0.5, Math.max(0.001, fpr));\n const t = Math.sqrt(-2.0 * Math.log(p));\n return (\n t -\n (2.515517 + t * (0.802853 + t * 0.010328)) /\n (1.0 + t * (1.432788 + t * (0.189269 + t * 0.001308)))\n );\n}\n\n/**\n * Strongly-typed internal options object produced by normalizing an\n * `IntentManagerConfig`. Every field has a concrete, non-optional value —\n * the constructor can use them directly without further null-coalescing.\n *\n * This covers **only** the config-precedence / default / clamping logic that\n * previously lived inside the `IntentManager` constructor. Fields that are\n * pure pass-through (e.g. `timer`, `storage`, `benchmark`, `onError`,\n * `baseline`, `lifecycleAdapter`, `asyncStorage`) are intentionally omitted\n * because they carry no defaulting or merging logic of their own.\n *\n * @internal Not part of the public package API. Field names and defaults may\n * change across any release without a semver-major bump.\n */\nexport interface ResolvedIntentManagerOptions {\n /* ── Scalar flags ──────────────────────────────────────────────────────── */\n botProtection: boolean;\n dwellTimeEnabled: boolean;\n crossTabSync: boolean;\n\n /* ── Holdout ───────────────────────────────────────────────────────────── */\n /** Clamped to [0, 100]. */\n holdoutPercent: number;\n\n /* ── Graph config (merged from top-level aliases + nested graph) ──────── */\n graphConfig: MarkovGraphConfig;\n\n /**\n * Resolved Laplace smoothing epsilon for trajectory scoring.\n * Falls back to `SMOOTHING_EPSILON` when the configured value is not\n * a finite positive number.\n */\n trajectorySmoothingEpsilon: number;\n\n /* ── Persistence ───────────────────────────────────────────────────────── */\n storageKey: string;\n persistDebounceMs: number;\n persistThrottleMs: number;\n\n /* ── Signal engine ─────────────────────────────────────────────────────── */\n eventCooldownMs: number;\n dwellTimeMinSamples: number;\n dwellTimeZScoreThreshold: number;\n enableBigrams: boolean;\n bigramFrequencyThreshold: number;\n driftMaxAnomalyRate: number;\n driftEvaluationWindowMs: number;\n hesitationCorrelationWindowMs: number;\n}\n\n/**\n * Pure function that normalizes an external `IntentManagerConfig` into a\n * fully-resolved `ResolvedIntentManagerOptions`.\n *\n * All precedence, default-value, and clamping rules are centralised here so\n * that the `IntentManager` constructor becomes pure wiring.\n *\n * **Precedence rules (unchanged from original):**\n * - `config.baselineMeanLL` wins over `config.graph.baselineMeanLL`\n * - `config.baselineStdLL` wins over `config.graph.baselineStdLL`\n * - `config.smoothingAlpha` wins over `config.graph.smoothingAlpha`\n *\n * **Clamping rules:**\n * - `holdoutConfig.percentage` is clamped to [0, 100].\n * - `graphConfig.smoothingEpsilon` must be a finite positive number; otherwise\n * the compile-time constant `SMOOTHING_EPSILON` is used.\n *\n * @internal Not part of the public package API.\n */\nexport function buildIntentManagerOptions(\n config: IntentManagerConfig = {},\n): ResolvedIntentManagerOptions {\n // ── Scalar flags ────────────────────────────────────────────────────────\n const botProtection = config.botProtection ?? true;\n const dwellTimeEnabled = config.dwellTime?.enabled ?? false;\n const crossTabSync = config.crossTabSync === true;\n\n // ── Holdout — clamped to [0, 100] ──────────────────────────────────────\n const rawHoldoutPct = config.holdoutConfig?.percentage;\n const holdoutPercent = Number.isFinite(rawHoldoutPct)\n ? Math.min(100, Math.max(0, rawHoldoutPct as number))\n : (rawHoldoutPct ?? 0);\n\n // ── Merge top-level convenience aliases into the nested graph config ────\n // Top-level fields take precedence when both are supplied.\n // targetFPR overrides divergenceThreshold when present.\n const graphFPR = config.graph?.targetFPR;\n const graphConfig: MarkovGraphConfig = {\n ...config.graph,\n baselineMeanLL: config.baselineMeanLL ?? config.graph?.baselineMeanLL,\n baselineStdLL: config.baselineStdLL ?? config.graph?.baselineStdLL,\n smoothingAlpha: config.smoothingAlpha ?? config.graph?.smoothingAlpha,\n divergenceThreshold: Number.isFinite(graphFPR)\n ? fprToZScore(graphFPR as number)\n : config.graph?.divergenceThreshold,\n };\n\n // ── Trajectory smoothing epsilon ────────────────────────────────────────\n const configuredSmoothing = graphConfig.smoothingEpsilon;\n const trajectorySmoothingEpsilon =\n typeof configuredSmoothing === 'number' &&\n Number.isFinite(configuredSmoothing) &&\n configuredSmoothing > 0\n ? configuredSmoothing\n : SMOOTHING_EPSILON;\n\n // ── Persistence ─────────────────────────────────────────────────────────\n const storageKey = config.storageKey ?? 'passive-intent';\n\n const rawPersistDebounce = config.persistDebounceMs;\n const persistDebounceMs =\n Number.isFinite(rawPersistDebounce) && (rawPersistDebounce as number) >= 0\n ? Math.floor(rawPersistDebounce as number)\n : 2000;\n\n const rawPersistThrottle = config.persistThrottleMs;\n const persistThrottleMs =\n Number.isFinite(rawPersistThrottle) && (rawPersistThrottle as number) >= 0\n ? Math.floor(rawPersistThrottle as number)\n : 0;\n\n // ── Signal engine ───────────────────────────────────────────────────────\n const rawEventCooldown = config.eventCooldownMs;\n const eventCooldownMs =\n Number.isFinite(rawEventCooldown) && (rawEventCooldown as number) >= 0\n ? Math.floor(rawEventCooldown as number)\n : 0;\n\n const rawDwellMinSamples = config.dwellTime?.minSamples;\n const dwellTimeMinSamples =\n Number.isFinite(rawDwellMinSamples) && (rawDwellMinSamples as number) >= 1\n ? Math.floor(rawDwellMinSamples as number)\n : 10;\n\n const dwellFPR = config.dwellTime?.targetFPR;\n const rawDwellZScore = config.dwellTime?.zScoreThreshold;\n const dwellTimeZScoreThreshold = Number.isFinite(dwellFPR)\n ? fprToZScore(dwellFPR as number)\n : Number.isFinite(rawDwellZScore) && (rawDwellZScore as number) > 0\n ? (rawDwellZScore as number)\n : 2.5;\n\n const enableBigrams = config.enableBigrams ?? false;\n\n const rawBigramThreshold = config.bigramFrequencyThreshold;\n const bigramFrequencyThreshold =\n Number.isFinite(rawBigramThreshold) && (rawBigramThreshold as number) >= 1\n ? Math.floor(rawBigramThreshold as number)\n : 5;\n\n const rawDriftMaxRate = config.driftProtection?.maxAnomalyRate;\n const driftMaxAnomalyRate = Number.isFinite(rawDriftMaxRate)\n ? Math.min(1, Math.max(0, rawDriftMaxRate as number))\n : 0.4;\n\n const rawDriftWindowMs = config.driftProtection?.evaluationWindowMs;\n const driftEvaluationWindowMs =\n Number.isFinite(rawDriftWindowMs) && (rawDriftWindowMs as number) > 0\n ? Math.floor(rawDriftWindowMs as number)\n : 300_000;\n\n const rawHesitationMs = config.hesitationCorrelationWindowMs;\n const hesitationCorrelationWindowMs =\n Number.isFinite(rawHesitationMs) && (rawHesitationMs as number) >= 0\n ? Math.floor(rawHesitationMs as number)\n : 30_000;\n\n return {\n botProtection,\n dwellTimeEnabled,\n crossTabSync,\n holdoutPercent,\n graphConfig,\n trajectorySmoothingEpsilon,\n storageKey,\n persistDebounceMs,\n persistThrottleMs,\n eventCooldownMs,\n dwellTimeMinSamples,\n dwellTimeZScoreThreshold,\n enableBigrams,\n bigramFrequencyThreshold,\n driftMaxAnomalyRate,\n driftEvaluationWindowMs,\n hesitationCorrelationWindowMs,\n };\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\ntype Listener<T> = (payload: T) => void;\n\n/**\n * Minimal, generic typed event emitter used by IntentManager to decouple\n * event production from consumption.\n *\n * - `on()` returns an unsubscribe function for clean teardown in SPA lifecycles.\n * - `emit()` iterates the listener set synchronously; listeners are called in\n * insertion order.\n * - `removeAll()` is used by `IntentManager.destroy()` to prevent memory leaks\n * when the instance is torn down.\n *\n * The class is intentionally kept framework-agnostic and dependency-free so it\n * can be extracted or tested in isolation without any engine context.\n */\nexport class EventEmitter<Events extends object> {\n private listeners = new Map<keyof Events, Set<Listener<any>>>();\n\n on<K extends keyof Events>(event: K, listener: Listener<Events[K]>): () => void {\n const set = this.listeners.get(event) ?? new Set<Listener<Events[K]>>();\n set.add(listener);\n this.listeners.set(event, set as Set<Listener<any>>);\n\n return () => {\n set.delete(listener);\n if (set.size === 0) this.listeners.delete(event);\n };\n }\n\n emit<K extends keyof Events>(event: K, payload: Events[K]): void {\n const set = this.listeners.get(event);\n if (!set) return;\n set.forEach((listener) => listener(payload));\n }\n\n removeAll(): void {\n this.listeners.clear();\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/**\n * Heuristic bot detector based on inter-event timing statistics.\n *\n * Two scoring criteria are evaluated over a sliding window of the last\n * `BOT_DETECTION_WINDOW` (10) `track()` call timestamps:\n *\n * 1. **Speed criterion** — each consecutive delta < `BOT_MIN_DELTA_MS` (50 ms)\n * increments the score. Real users rarely navigate faster than 50 ms\n * per page; automation tools do it consistently.\n *\n * 2. **Variance criterion** — if the variance of all deltas in the window\n * falls below `BOT_MAX_VARIANCE` (100 ms²), the score gains 1 extra point.\n * Human timing has natural jitter; bots that pace calls with `setInterval`\n * produce near-zero variance even if each individual call is plausible.\n *\n * The `isSuspectedBot` flag flips to `true` when the combined score ≥\n * `BOT_SCORE_THRESHOLD` (5) and stays `true` until enough slow, high-\n * variance calls push the score back below the threshold.\n *\n * **No false negatives by design:** if bot protection is disabled at the\n * `IntentManager` level, this class is never called. When enabled, the\n * tradeoff is a small false-positive risk for users on very fast connections\n * or programmatic navigation (e.g. router `push()` in rapid succession).\n */\n\nconst BOT_DETECTION_WINDOW = 10;\n/** Minimum realistic human inter-navigation delta in milliseconds. */\nconst BOT_MIN_DELTA_MS = 50;\n/** Maximum variance (ms²) for the timing-uniformity criterion. */\nconst BOT_MAX_VARIANCE = 100;\n/** Combined score threshold above which the session is classified as a bot. */\nconst BOT_SCORE_THRESHOLD = 5;\n\nexport class EntropyGuard {\n private isSuspectedBot = false;\n private readonly trackTimestamps: number[] = new Array(BOT_DETECTION_WINDOW).fill(0);\n private trackTimestampIndex = 0;\n private trackTimestampCount = 0;\n\n record(timestamp: number): { suspected: boolean; transitionedToBot: boolean } {\n this.trackTimestamps[this.trackTimestampIndex] = timestamp;\n this.trackTimestampIndex = (this.trackTimestampIndex + 1) % BOT_DETECTION_WINDOW;\n if (this.trackTimestampCount < BOT_DETECTION_WINDOW) {\n this.trackTimestampCount += 1;\n }\n\n const previous = this.isSuspectedBot;\n this.evaluate();\n return {\n suspected: this.isSuspectedBot,\n transitionedToBot: this.isSuspectedBot && !previous,\n };\n }\n\n get suspected(): boolean {\n return this.isSuspectedBot;\n }\n\n private evaluate(): void {\n const count = this.trackTimestampCount;\n if (count < 3) return;\n\n let windowBotScore = 0;\n const deltas: number[] = [];\n\n // Ring-buffer oldest entry: if the window is not yet full, oldest is index 0;\n // otherwise it's the slot that was written next (trackTimestampIndex wraps around).\n const oldestIndex = count < BOT_DETECTION_WINDOW ? 0 : this.trackTimestampIndex;\n\n for (let i = 0; i < count - 1; i += 1) {\n const currIdx = (oldestIndex + i) % BOT_DETECTION_WINDOW;\n const nextIdx = (oldestIndex + i + 1) % BOT_DETECTION_WINDOW;\n const delta = this.trackTimestamps[nextIdx] - this.trackTimestamps[currIdx];\n deltas.push(delta);\n if (delta >= 0 && delta < BOT_MIN_DELTA_MS) {\n windowBotScore += 1;\n }\n }\n\n if (deltas.length >= 2) {\n let mean = 0;\n for (let i = 0; i < deltas.length; i += 1) {\n mean += deltas[i];\n }\n mean /= deltas.length;\n\n let variance = 0;\n for (let i = 0; i < deltas.length; i += 1) {\n const d = deltas[i] - mean;\n variance += d * d;\n }\n variance /= deltas.length;\n\n // variance is always ≥ 0 (sum of squares / count), so only the upper\n // bound matters: low variance means unnaturally regular timing.\n if (variance < BOT_MAX_VARIANCE) {\n windowBotScore += 1;\n }\n }\n\n this.isSuspectedBot = windowBotScore >= BOT_SCORE_THRESHOLD;\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/**\n * Welford's online algorithm state for a single numeric stream.\n *\n * Maintains mean and variance in a single pass without storing the raw\n * samples, making it O(1) per update and O(1) in memory regardless of\n * session length. All three fields are required to resume a running\n * computation (e.g. across persisted checkpoints).\n *\n * Reference: Welford, B.P. (1962), \"Note on a method for calculating\n * corrected sums of squares and products\", Technometrics 4(3):419–420.\n */\nexport interface DwellStats {\n /** Number of observations recorded so far. */\n count: number;\n /** Running arithmetic mean of dwell times in milliseconds. */\n meanMs: number;\n /**\n * Welford’s running sum of squared deviations from the mean.\n * Divide by `count` (biased) or `count - 1` (unbiased Bessel-corrected)\n * to obtain variance. `dwellStd()` uses the biased estimator intentionally\n * because we care about the current session’s distribution, not an\n * unbiased population estimate.\n */\n m2: number;\n}\n\n/**\n * Update a `DwellStats` accumulator with a new observation using\n * Welford’s algorithm. Creates a fresh accumulator if `current` is\n * undefined (i.e. first observation for a state).\n */\nexport function updateDwellStats(current: DwellStats | undefined, dwellMs: number): DwellStats {\n const previousCount = current?.count ?? 0;\n const previousMean = current?.meanMs ?? 0;\n const previousM2 = current?.m2 ?? 0;\n const count = previousCount + 1;\n const delta = dwellMs - previousMean;\n const meanMs = previousMean + delta / count;\n const delta2 = dwellMs - meanMs;\n const m2 = previousM2 + delta * delta2;\n return { count, meanMs, m2 };\n}\n\n/**\n * Biased population standard deviation derived from Welford state.\n * Returns 0 when fewer than 2 samples have been recorded (no meaningful\n * spread can be computed from a single data point).\n */\nexport function dwellStd(stats: DwellStats): number {\n if (stats.count < 2) return 0;\n return Math.sqrt(stats.m2 / stats.count);\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/**\n * AnomalyDispatcher — the single point that converts evaluator decisions\n * into observable side-effects.\n *\n * Responsibilities:\n * 1. **Cooldown policy** — suppresses events that fire more frequently than\n * `eventCooldownMs` per event type.\n * 2. **Holdout suppression** — events are never forwarded to the emitter\n * when `assignmentGroup === 'control'`.\n * 3. **Telemetry accounting** — `anomaliesFired` is incremented once per\n * dispatched decision that passes the cooldown gate.\n * 4. **Drift protection** — `driftPolicy.recordAnomaly()` is called for\n * every `TrajectoryDecision` *before* the cooldown check, preserving the\n * original semantics: drift accounting is done on the raw signal, not on\n * the rate-limited emission.\n * 5. **Hesitation correlation** — bookkeeping timestamps are updated after a\n * successful dispatch, and `hesitation_detected` is emitted whenever\n * both a trajectory anomaly and a positive-z-score dwell anomaly fall\n * within `hesitationCorrelationWindowMs`.\n *\n * All of the above were previously scattered across `SignalEngine`. Moving\n * them here lets the evaluator methods return pure decision values and keeps\n * the dispatcher as the only component that touches the emitter or mutates\n * any session state.\n */\n\nimport type { EventEmitter } from './event-emitter.js';\nimport type { IntentEventMap } from '../types/events.js';\nimport type { TimerAdapter } from '../adapters.js';\nimport type { DriftProtectionPolicy } from './policies/drift-protection-policy.js';\nimport type { AnomalyDecision } from './anomaly-decisions.js';\n\n/** Compile-time exhaustiveness check. */\nfunction assertNever(x: never): never {\n throw new Error(`Unhandled AnomalyDecision kind: ${(x as { kind: string }).kind}`);\n}\n\n/**\n * Structural view of the event emitter used by the dispatcher.\n *\n * This is intentionally minimal so that external consumers do not need access\n * to the concrete EventEmitter class, only to an object with the required\n * methods.\n */\nexport interface AnomalyEventEmitter {\n emit: EventEmitter<IntentEventMap>['emit'];\n on: EventEmitter<IntentEventMap>['on'];\n removeAll: EventEmitter<IntentEventMap>['removeAll'];\n}\n\n/**\n * Structural view of the drift-protection policy required by the dispatcher.\n */\nexport interface DriftProtectionPolicyLike {\n recordAnomaly: DriftProtectionPolicy['recordAnomaly'];\n readonly isDrifted: DriftProtectionPolicy['isDrifted'];\n readonly baselineStatus: DriftProtectionPolicy['baselineStatus'];\n}\n\nexport interface AnomalyDispatcherConfig {\n emitter: AnomalyEventEmitter;\n timer: TimerAdapter;\n assignmentGroup: 'treatment' | 'control';\n eventCooldownMs: number;\n hesitationCorrelationWindowMs: number;\n driftPolicy: DriftProtectionPolicyLike;\n}\n\nexport class AnomalyDispatcher {\n private readonly emitter: AnomalyEventEmitter;\n private readonly timer: TimerAdapter;\n private readonly assignmentGroup: 'treatment' | 'control';\n private readonly eventCooldownMs: number;\n private readonly hesitationCorrelationWindowMs: number;\n private readonly driftPolicy: DriftProtectionPolicyLike;\n\n /* Cooldown gating per event type */\n private readonly lastEmittedAt: Record<\n 'high_entropy' | 'trajectory_anomaly' | 'dwell_time_anomaly',\n number\n > = {\n high_entropy: -Infinity,\n trajectory_anomaly: -Infinity,\n dwell_time_anomaly: -Infinity,\n };\n\n /* Hesitation correlation state */\n private lastTrajectoryAnomalyAt = -Infinity;\n private lastTrajectoryAnomalyZScore = 0;\n private lastDwellAnomalyAt = -Infinity;\n private lastDwellAnomalyZScore = 0;\n private lastDwellAnomalyState = '';\n\n /* Session-scoped anomaly counter */\n private anomaliesFiredInternal = 0;\n\n constructor(config: AnomalyDispatcherConfig) {\n this.emitter = config.emitter;\n this.timer = config.timer;\n this.assignmentGroup = config.assignmentGroup;\n this.eventCooldownMs = config.eventCooldownMs;\n this.hesitationCorrelationWindowMs = config.hesitationCorrelationWindowMs;\n this.driftPolicy = config.driftPolicy;\n }\n\n /* ================================================================== */\n /* Telemetry getter */\n /* ================================================================== */\n\n get anomaliesFired(): number {\n return this.anomaliesFiredInternal;\n }\n\n /* ================================================================== */\n /* Dispatch */\n /* ================================================================== */\n\n /**\n * Apply cooldown policy, holdout suppression, telemetry increment, and all\n * emitter side-effects for a single evaluator decision.\n *\n * Passing `null` is a no-op and is the normal case when no anomaly was\n * detected on the current transition.\n *\n * @param decision - The decision returned by an evaluator, or `null`.\n */\n dispatch(decision: AnomalyDecision | null): void {\n if (decision === null) return;\n\n // ── Drift accounting (trajectory only, before cooldown) ─────────────────\n // Must mirror the original semantics: every detected trajectory anomaly\n // counts against the drift-protection window, regardless of whether the\n // event is actually emitted.\n if (decision.kind === 'trajectory_anomaly') {\n this.driftPolicy.recordAnomaly();\n }\n\n // ── Cooldown gate ─────────────────────────────────────────────────────────\n const now = this.timer.now();\n if (\n this.eventCooldownMs > 0 &&\n now - this.lastEmittedAt[decision.kind] < this.eventCooldownMs\n ) {\n return;\n }\n\n // ── Update cooldown timestamp ─────────────────────────────────────────────\n this.lastEmittedAt[decision.kind] = now;\n\n // ── Telemetry increment ───────────────────────────────────────────────────\n this.anomaliesFiredInternal += 1;\n\n // ── Holdout suppression + emission ───────────────────────────────────────\n if (this.assignmentGroup !== 'control') {\n switch (decision.kind) {\n case 'high_entropy':\n this.emitter.emit('high_entropy', decision.payload);\n break;\n case 'trajectory_anomaly':\n this.emitter.emit('trajectory_anomaly', decision.payload);\n break;\n case 'dwell_time_anomaly':\n this.emitter.emit('dwell_time_anomaly', decision.payload);\n break;\n default:\n assertNever(decision);\n }\n }\n\n // ── Hesitation correlation bookkeeping ────────────────────────────────────\n if (decision.kind === 'trajectory_anomaly') {\n this.lastTrajectoryAnomalyAt = now;\n this.lastTrajectoryAnomalyZScore = decision.payload.zScore;\n this.maybeEmitHesitation(now);\n } else if (decision.kind === 'dwell_time_anomaly' && decision.isPositiveZScore) {\n this.lastDwellAnomalyAt = now;\n this.lastDwellAnomalyZScore = decision.payload.zScore;\n this.lastDwellAnomalyState = decision.payload.state;\n this.maybeEmitHesitation(now);\n }\n }\n\n /* ================================================================== */\n /* Hesitation Correlation (private) */\n /* ================================================================== */\n\n private maybeEmitHesitation(now: number): void {\n const correlated =\n now - this.lastTrajectoryAnomalyAt < this.hesitationCorrelationWindowMs &&\n now - this.lastDwellAnomalyAt < this.hesitationCorrelationWindowMs;\n\n if (!correlated) return;\n\n // Consume both timestamps so the same pair cannot trigger twice.\n this.lastTrajectoryAnomalyAt = -Infinity;\n this.lastDwellAnomalyAt = -Infinity;\n\n if (this.assignmentGroup !== 'control') {\n this.emitter.emit('hesitation_detected', {\n state: this.lastDwellAnomalyState,\n trajectoryZScore: this.lastTrajectoryAnomalyZScore,\n dwellZScore: this.lastDwellAnomalyZScore,\n });\n }\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport type { BenchmarkRecorder } from '../performance-instrumentation.js';\nimport type { TimerAdapter } from '../adapters.js';\nimport { MarkovGraph } from '../core/markov.js';\nimport { EntropyGuard } from './entropy-guard.js';\nimport { dwellStd, updateDwellStats } from './dwell.js';\nimport type { DwellStats } from './dwell.js';\nimport { EventEmitter } from './event-emitter.js';\nimport type { AnomalyEventEmitter, DriftProtectionPolicyLike } from './anomaly-dispatcher.js';\nimport { MIN_SAMPLE_TRANSITIONS, MIN_WINDOW_LENGTH, MAX_WINDOW_LENGTH } from './constants.js';\nimport type { PassiveIntentTelemetry } from '../types/events.js';\nimport { AnomalyDispatcher } from './anomaly-dispatcher.js';\nimport type {\n AnomalyDecision,\n EntropyDecision,\n TrajectoryDecision,\n DwellDecision,\n} from './anomaly-decisions.js';\n\nexport { EventEmitter };\n\nfunction getConfidence(sampleSize: number): 'low' | 'medium' | 'high' {\n if (sampleSize < 10) return 'low';\n if (sampleSize < 30) return 'medium';\n return 'high';\n}\n/**\n * Configuration surface for SignalEngine.\n * All values are resolved and defaulted by IntentManager before being passed in.\n */\nexport interface SignalEngineConfig {\n graph: MarkovGraph;\n baseline: MarkovGraph | null;\n timer: TimerAdapter;\n benchmark: BenchmarkRecorder;\n emitter: AnomalyEventEmitter;\n assignmentGroup: 'treatment' | 'control';\n eventCooldownMs: number;\n dwellTimeMinSamples: number;\n dwellTimeZScoreThreshold: number;\n hesitationCorrelationWindowMs: number;\n trajectorySmoothingEpsilon: number;\n /** Drift protection policy — owns the rolling evaluation window and drifted flag. */\n driftPolicy: DriftProtectionPolicyLike;\n}\n\n/**\n * SignalEngine — pure computation kernel for all anomaly signals.\n *\n * ## Responsibilities\n * Each public `evaluate*` method is a **pure evaluator**: it reads engine state\n * and returns a typed decision object (or `null`), but never emits events,\n * mutates cooldown timestamps, or touches telemetry counters.\n *\n * Owns:\n * - `EntropyGuard` — bot-detection sliding window\n * - Per-state Welford accumulators for dwell-time statistics\n * - Session-scoped `transitionsEvaluated` counter\n *\n * ## What it does NOT own\n * All side-effects have been moved to `AnomalyDispatcher`, which is composed\n * internally and exposed through `dispatch()`:\n * - Cooldown gating per event type\n * - Holdout (control-group) suppression\n * - `anomaliesFired` telemetry increment\n * - Drift-protection `recordAnomaly()` calls\n * - Emitter calls for `high_entropy`, `trajectory_anomaly`, `dwell_time_anomaly`\n * - Hesitation correlation and `hesitation_detected` emission\n *\n * ## Usage pattern\n * ```ts\n * signalEngine.dispatch(signalEngine.evaluateEntropy(state));\n * signalEngine.dispatch(signalEngine.evaluateTrajectory(from, to, trajectory));\n * signalEngine.dispatch(signalEngine.evaluateDwellTime(state, dwellMs));\n * ```\n *\n * **Do not add side-effects to evaluator methods.** If a new signal type\n * requires emission or shared-state mutation, add a new `evaluate*` that\n * returns a decision type and handle it inside `AnomalyDispatcher.dispatch()`.\n *\n * IntentManager passes already-resolved config values and read-only trajectory\n * slices into each evaluation method; no I/O occurs here.\n */\nexport class SignalEngine {\n private readonly graph: MarkovGraph;\n private readonly baseline: MarkovGraph | null;\n private readonly benchmark: BenchmarkRecorder;\n private readonly dwellTimeMinSamples: number;\n private readonly dwellTimeZScoreThreshold: number;\n private readonly trajectorySmoothingEpsilon: number;\n private readonly driftPolicy: DriftProtectionPolicyLike;\n\n /* Bot detection */\n private readonly entropyGuard = new EntropyGuard();\n\n /* Dwell-time Welford accumulators — session-scoped, never persisted */\n private readonly dwellStats = new Map<string, DwellStats>();\n\n /* Dispatcher — owns cooldown, hesitation, telemetry, and emitter side-effects */\n private readonly dispatcher: AnomalyDispatcher;\n\n /* Session-scoped transition counter */\n private transitionsEvaluatedInternal = 0;\n\n constructor(config: SignalEngineConfig) {\n this.graph = config.graph;\n this.baseline = config.baseline;\n this.benchmark = config.benchmark;\n this.dwellTimeMinSamples = config.dwellTimeMinSamples;\n this.dwellTimeZScoreThreshold = config.dwellTimeZScoreThreshold;\n this.trajectorySmoothingEpsilon = config.trajectorySmoothingEpsilon;\n this.driftPolicy = config.driftPolicy;\n\n this.dispatcher = new AnomalyDispatcher({\n emitter: config.emitter,\n timer: config.timer,\n assignmentGroup: config.assignmentGroup,\n eventCooldownMs: config.eventCooldownMs,\n hesitationCorrelationWindowMs: config.hesitationCorrelationWindowMs,\n driftPolicy: config.driftPolicy,\n });\n }\n\n /* ================================================================== */\n /* Telemetry Getters */\n /* ================================================================== */\n\n get suspected(): boolean {\n return this.entropyGuard.suspected;\n }\n\n get transitionsEvaluated(): number {\n return this.transitionsEvaluatedInternal;\n }\n\n get anomaliesFired(): number {\n return this.dispatcher.anomaliesFired;\n }\n\n get isBaselineDrifted(): boolean {\n return this.driftPolicy.isDrifted;\n }\n\n get baselineStatus(): PassiveIntentTelemetry['baselineStatus'] {\n return this.driftPolicy.baselineStatus;\n }\n\n /* ================================================================== */\n /* Bot Protection */\n /* ================================================================== */\n\n /**\n * Record a `track()` timestamp into the EntropyGuard and return bot state.\n * If the guard transitions to suspected-bot, IntentManager emits `bot_detected`.\n */\n recordBotCheck(now: number): { suspected: boolean; transitionedToBot: boolean } {\n return this.entropyGuard.record(now);\n }\n\n /* ================================================================== */\n /* Transition Accounting */\n /* ================================================================== */\n\n /**\n * Increment the session-scoped transition counter.\n *\n * Bigram accounting was moved to `BigramPolicy.onTransition()` as part of\n * the policy refactor; this method's sole responsibility is updating the\n * `transitionsEvaluated` telemetry counter.\n */\n recordTransition(_from: string, _to: string, _trajectory: readonly string[]): void {\n this.transitionsEvaluatedInternal += 1;\n // Bigram accounting is now handled by BigramPolicy.onTransition().\n }\n\n /* ================================================================== */\n /* Dispatch delegation */\n /* ================================================================== */\n\n /**\n * Forward a decision produced by any evaluator to the AnomalyDispatcher.\n * Passing `null` is a safe no-op and is the common case when no anomaly\n * was detected.\n */\n dispatch(decision: AnomalyDecision | null): void {\n this.dispatcher.dispatch(decision);\n }\n\n /* ================================================================== */\n /* Entropy Evaluation */\n /* ================================================================== */\n\n /**\n * Evaluate the entropy of the current state and return a decision when an\n * anomaly is detected, or `null` when the state is normal or below the\n * minimum-sample threshold.\n *\n * This method is a **pure evaluator**: it reads state but performs no\n * side-effects. Call `dispatch(evaluateEntropy(state))` to apply\n * cooldown, holdout suppression, and emission.\n */\n evaluateEntropy(state: string): EntropyDecision | null {\n const start = this.benchmark.now();\n\n if (this.entropyGuard.suspected) {\n this.benchmark.record('entropyComputation', start);\n return null;\n }\n\n if (this.graph.rowTotal(state) < MIN_SAMPLE_TRANSITIONS) {\n this.benchmark.record('entropyComputation', start);\n return null;\n }\n\n const entropy = this.graph.entropyForState(state);\n const normalizedEntropy = this.graph.normalizedEntropyForState(state);\n\n this.benchmark.record('entropyComputation', start);\n\n if (normalizedEntropy >= this.graph.highEntropyThreshold) {\n return { kind: 'high_entropy', payload: { state, entropy, normalizedEntropy } };\n }\n\n return null;\n }\n\n /* ================================================================== */\n /* Trajectory Anomaly Detection */\n /* ================================================================== */\n\n /**\n * Evaluate the current trajectory against the baseline graph and return a\n * `TrajectoryDecision` when a z-score (or raw LL) anomaly is detected, or\n * `null` when the trajectory is normal or any precondition is unmet.\n *\n * This method is a **pure evaluator**: it reads state but performs no\n * side-effects. Drift accounting (`driftPolicy.recordAnomaly()`) is\n * intentionally deferred to `AnomalyDispatcher.dispatch()` where it is\n * applied *before* the cooldown check, preserving the original semantics.\n *\n * @param from Departing state of the most recent transition.\n * @param to Arriving state of the most recent transition.\n * @param trajectory Read-only snapshot of the sliding trajectory window.\n */\n evaluateTrajectory(\n from: string,\n to: string,\n trajectory: readonly string[],\n ): TrajectoryDecision | null {\n const start = this.benchmark.now();\n\n if (this.driftPolicy.isDrifted) {\n this.benchmark.record('divergenceComputation', start);\n return null;\n }\n\n if (this.entropyGuard.suspected) {\n this.benchmark.record('divergenceComputation', start);\n return null;\n }\n\n if (trajectory.length < MIN_WINDOW_LENGTH) {\n this.benchmark.record('divergenceComputation', start);\n return null;\n }\n\n if (!this.baseline) {\n this.benchmark.record('divergenceComputation', start);\n return null;\n }\n\n const real = MarkovGraph.logLikelihoodTrajectory(\n this.graph,\n trajectory,\n this.trajectorySmoothingEpsilon,\n );\n const expected = MarkovGraph.logLikelihoodTrajectory(\n this.baseline,\n trajectory,\n this.trajectorySmoothingEpsilon,\n );\n\n const N = Math.max(1, trajectory.length - 1);\n const expectedAvg = expected / N;\n const threshold = -Math.abs(this.graph.divergenceThreshold);\n\n const hasCalibratedBaseline =\n typeof this.graph.baselineMeanLL === 'number' &&\n typeof this.graph.baselineStdLL === 'number' &&\n Number.isFinite(this.graph.baselineMeanLL) &&\n Number.isFinite(this.graph.baselineStdLL) &&\n this.graph.baselineStdLL > 0;\n\n const adjustedStd = hasCalibratedBaseline\n ? this.graph.baselineStdLL * Math.sqrt(MAX_WINDOW_LENGTH / N)\n : 0;\n\n const zScore = hasCalibratedBaseline\n ? (expectedAvg - this.graph.baselineMeanLL) / adjustedStd\n : expectedAvg;\n\n const shouldEmit = hasCalibratedBaseline ? zScore <= threshold : expectedAvg <= threshold;\n\n this.benchmark.record('divergenceComputation', start);\n\n if (shouldEmit) {\n const sampleSize = this.graph.rowTotal(from);\n return {\n kind: 'trajectory_anomaly',\n payload: {\n stateFrom: from,\n stateTo: to,\n realLogLikelihood: real,\n expectedBaselineLogLikelihood: expected,\n zScore,\n sampleSize,\n confidence: getConfidence(sampleSize),\n },\n };\n }\n\n return null;\n }\n\n /* ================================================================== */\n /* Dwell-Time Anomaly Detection */\n /* ================================================================== */\n\n /**\n * Evaluate dwell time on the *previous* state via Welford's online algorithm\n * and return a `DwellDecision` when the z-score exceeds the configured\n * threshold, or `null` otherwise.\n *\n * The Welford accumulator is always updated regardless of whether a decision\n * is produced — this ensures the running mean/std improves with every sample.\n *\n * This method is a **pure evaluator** with one intentional statistical\n * side-effect: the per-state `dwellStats` accumulator is mutated so that\n * successive calls converge on accurate mean and standard-deviation estimates.\n * No events are emitted here.\n */\n evaluateDwellTime(state: string, dwellMs: number): DwellDecision | null {\n // Gating (dwellTimeEnabled) is handled by DwellTimePolicy; this method\n // is only called when the policy exists and has decided dwell should be\n // evaluated for this transition.\n if (dwellMs <= 0) return null;\n\n const updated = updateDwellStats(this.dwellStats.get(state), dwellMs);\n this.dwellStats.set(state, updated);\n\n if (updated.count < this.dwellTimeMinSamples) return null;\n\n const std = dwellStd(updated);\n if (std <= 0) return null;\n\n const zScore = (dwellMs - updated.meanMs) / std;\n\n if (Math.abs(zScore) >= this.dwellTimeZScoreThreshold) {\n const sampleSize = updated.count;\n return {\n kind: 'dwell_time_anomaly',\n payload: {\n state,\n dwellMs,\n meanMs: updated.meanMs,\n stdMs: std,\n zScore,\n sampleSize,\n confidence: getConfidence(sampleSize),\n },\n isPositiveZScore: zScore > 0,\n };\n }\n\n return null;\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport type {\n AsyncStorageAdapter,\n StorageAdapter,\n TimerAdapter,\n TimerHandle,\n} from '../adapters.js';\nimport type { MarkovGraph, SerializedMarkovGraph } from '../core/markov.js';\nimport type { BloomFilter } from '../core/bloom.js';\nimport { uint8ToBase64 } from '../persistence/codec.js';\nimport type { PassiveIntentError, PassiveIntentTelemetry } from '../types/events.js';\n\nexport interface PersistedPayload {\n bloomBase64: string;\n graphBinary?: string;\n graph?: SerializedMarkovGraph;\n}\n\nexport interface PersistStrategyContext {\n getStorageKey(): string;\n getStorage(): StorageAdapter;\n getAsyncStorage(): AsyncStorageAdapter | null;\n getTimer(): TimerAdapter;\n getThrottleMs(): number;\n getDebounceMs(): number;\n getGraphAndBloom(): { graph: MarkovGraph; bloom: BloomFilter } | null;\n\n isClosed(): boolean;\n isDirty(): boolean;\n clearDirty(): void;\n markDirty(): void;\n\n setEngineHealth(health: PassiveIntentTelemetry['engineHealth']): void;\n reportError(code: PassiveIntentError['code'], message: string, err: unknown): void;\n}\n\nexport interface PersistStrategy {\n persist(): void;\n flushNow(): void;\n close(): void;\n}\n\nexport abstract class BasePersistStrategy implements PersistStrategy {\n protected lastPersistedAt = -Infinity;\n protected throttleTimer: TimerHandle | null = null;\n protected isClosedFlag = false;\n\n constructor(protected readonly ctx: PersistStrategyContext) {}\n\n abstract persist(): void;\n\n protected serialize(): string | null {\n const data = this.ctx.getGraphAndBloom();\n if (!data) return null;\n const { graph, bloom } = data;\n\n this.ctx.setEngineHealth('pruning_active');\n try {\n graph.prune();\n } finally {\n this.ctx.setEngineHealth('healthy');\n }\n\n let graphBinary: string;\n try {\n const graphBytes = graph.toBinary();\n graphBinary = uint8ToBase64(graphBytes);\n } catch (err) {\n this.ctx.reportError('SERIALIZE', err instanceof Error ? err.message : String(err), err);\n return null;\n }\n\n return JSON.stringify({\n bloomBase64: bloom.toBase64(),\n graphBinary,\n });\n }\n\n flushNow(): void {\n if (this.throttleTimer !== null) {\n this.ctx.getTimer().clearTimeout(this.throttleTimer);\n this.throttleTimer = null;\n }\n this.lastPersistedAt = -Infinity;\n this.persist();\n }\n\n close(): void {\n this.isClosedFlag = true;\n if (this.throttleTimer !== null) {\n this.ctx.getTimer().clearTimeout(this.throttleTimer);\n this.throttleTimer = null;\n }\n }\n\n protected checkThrottle(): boolean {\n const throttleMs = this.ctx.getThrottleMs();\n if (throttleMs > 0 && !this.isClosedFlag) {\n const now = this.ctx.getTimer().now();\n const elapsed = now - this.lastPersistedAt;\n if (elapsed < throttleMs) {\n if (this.throttleTimer === null) {\n const remainingMs = throttleMs - elapsed;\n this.throttleTimer = this.ctx.getTimer().setTimeout(() => {\n this.throttleTimer = null;\n this.persist();\n }, remainingMs);\n }\n return true;\n }\n }\n return false;\n }\n}\n\nexport class SyncPersistStrategy extends BasePersistStrategy {\n persist(): void {\n if (!this.ctx.isDirty() || !this.ctx.getGraphAndBloom()) return;\n\n if (this.checkThrottle()) return;\n\n const payload = this.serialize();\n if (!payload) return;\n\n try {\n this.ctx.getStorage().setItem(this.ctx.getStorageKey(), payload);\n this.ctx.clearDirty();\n this.lastPersistedAt = this.ctx.getTimer().now();\n this.ctx.setEngineHealth('healthy');\n } catch (err) {\n const isQuota =\n err instanceof Error &&\n (err.name === 'QuotaExceededError' || err.message.toLowerCase().includes('quota'));\n if (isQuota) {\n this.ctx.setEngineHealth('quota_exceeded');\n }\n this.ctx.reportError(\n isQuota ? 'QUOTA_EXCEEDED' : 'STORAGE_WRITE',\n err instanceof Error ? err.message : String(err),\n err,\n );\n }\n }\n}\n\nexport class AsyncPersistStrategy extends BasePersistStrategy {\n private isAsyncWriting = false;\n private hasPendingAsyncPersist = false;\n private asyncWriteFailCount = 0;\n private retryTimer: TimerHandle | null = null;\n\n persist(): void {\n if (!this.ctx.isDirty() || !this.ctx.getGraphAndBloom()) return;\n\n if (this.isAsyncWriting) {\n this.hasPendingAsyncPersist = true;\n return;\n }\n\n if (this.checkThrottle()) return;\n\n const payload = this.serialize();\n if (!payload) return;\n\n this.isAsyncWriting = true;\n this.hasPendingAsyncPersist = false;\n this.ctx.clearDirty();\n\n const asyncStorage = this.ctx.getAsyncStorage()!;\n let setItemPromise: Promise<void>;\n try {\n setItemPromise = asyncStorage.setItem(this.ctx.getStorageKey(), payload);\n } catch (err: unknown) {\n this.handleAsyncWriteError(err);\n return;\n }\n\n setItemPromise\n .then(() => {\n this.isAsyncWriting = false;\n this.asyncWriteFailCount = 0;\n this.lastPersistedAt = this.ctx.getTimer().now();\n this.ctx.setEngineHealth('healthy');\n if (this.hasPendingAsyncPersist || this.ctx.isDirty()) {\n this.hasPendingAsyncPersist = false;\n this.persist();\n }\n })\n .catch((err: unknown) => {\n this.handleAsyncWriteError(err);\n });\n }\n\n private handleAsyncWriteError(err: unknown): void {\n this.isAsyncWriting = false;\n this.ctx.markDirty();\n this.asyncWriteFailCount += 1;\n\n const isQuota =\n err instanceof Error &&\n (err.name === 'QuotaExceededError' || err.message.toLowerCase().includes('quota'));\n if (isQuota) {\n this.ctx.setEngineHealth('quota_exceeded');\n }\n this.ctx.reportError(\n isQuota ? 'QUOTA_EXCEEDED' : 'STORAGE_WRITE',\n err instanceof Error ? err.message : String(err),\n err,\n );\n\n this.hasPendingAsyncPersist = false;\n if (!this.isClosedFlag && this.asyncWriteFailCount === 1) {\n this.schedulePersist();\n }\n }\n\n private schedulePersist(): void {\n if (this.isClosedFlag) return;\n if (this.retryTimer !== null) {\n this.ctx.getTimer().clearTimeout(this.retryTimer);\n }\n this.retryTimer = this.ctx.getTimer().setTimeout(() => {\n this.retryTimer = null;\n this.persist();\n }, this.ctx.getDebounceMs());\n }\n\n override flushNow(): void {\n if (this.retryTimer !== null) {\n this.ctx.getTimer().clearTimeout(this.retryTimer);\n this.retryTimer = null;\n }\n super.flushNow();\n }\n\n override close(): void {\n super.close();\n if (this.retryTimer !== null) {\n this.ctx.getTimer().clearTimeout(this.retryTimer);\n this.retryTimer = null;\n }\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport type { AsyncStorageAdapter, StorageAdapter, TimerAdapter } from '../adapters.js';\nimport type { MarkovGraph } from '../core/markov.js';\nimport type { BloomFilter } from '../core/bloom.js';\nimport { BloomFilter as BloomFilterClass } from '../core/bloom.js';\nimport { MarkovGraph as MarkovGraphClass } from '../core/markov.js';\nimport { base64ToUint8 } from '../persistence/codec.js';\nimport type {\n MarkovGraphConfig,\n PassiveIntentError,\n PassiveIntentTelemetry,\n} from '../types/events.js';\n\nimport { SyncPersistStrategy, AsyncPersistStrategy } from './persistence-strategies.js';\nimport type {\n PersistStrategy,\n PersistStrategyContext,\n PersistedPayload,\n} from './persistence-strategies.js';\n\n/**\n * Configuration for PersistenceCoordinator.\n * All values are resolved and defaulted by IntentManager before being passed in.\n */\nexport interface PersistenceCoordinatorConfig {\n storageKey: string;\n persistDebounceMs: number;\n persistThrottleMs: number;\n storage: StorageAdapter;\n asyncStorage: AsyncStorageAdapter | null;\n timer: TimerAdapter;\n onError?: (err: PassiveIntentError) => void;\n}\n\n/**\n * PersistenceCoordinator — owns all write/read/retry orchestration.\n *\n * Responsibilities:\n * - `restore()` — deserialise Bloom filter + Markov graph from storage on startup\n * - `persist()` — prune → serialize → write (sync or async path)\n * - Dirty-flag short-circuit (no-op when nothing changed)\n * - Throttle gate with trailing-flush timer\n * - Async write guard (prevents overlapping `setItem` calls)\n * - One-shot retry on first consecutive async write failure\n * - `engineHealth` status flag exposed to telemetry\n * - `flushNow()` — bypasses throttle for teardown/forced flush\n *\n * Call `attach(graph, bloom)` once after restoring/constructing the Markov graph\n * and Bloom filter so the coordinator can serialise them on each `persist()`.\n */\nexport class PersistenceCoordinator implements PersistStrategyContext {\n private readonly storageKey: string;\n private readonly persistDebounceMs: number;\n private readonly persistThrottleMs: number;\n private readonly storage: StorageAdapter;\n private readonly asyncStorage: AsyncStorageAdapter | null;\n private readonly timer: TimerAdapter;\n private readonly strategy: PersistStrategy;\n private readonly onErrorCb?: (err: PassiveIntentError) => void;\n\n /* Late-bound after attach() */\n private graphInstance: MarkovGraph | null = null;\n private bloomInstance: BloomFilter | null = null;\n\n /* Lifecycle */\n private isClosedFlag = false;\n\n /* Mutable coordination flags — internal only */\n private isDirtyFlag = false;\n private engineHealthInternal: PassiveIntentTelemetry['engineHealth'] = 'healthy';\n\n /** Signal that new state has been written and needs to be persisted. */\n markDirty(): void {\n this.isDirtyFlag = true;\n }\n\n constructor(config: PersistenceCoordinatorConfig) {\n this.storageKey = config.storageKey;\n this.persistDebounceMs = config.persistDebounceMs;\n this.persistThrottleMs = config.persistThrottleMs;\n this.storage = config.storage;\n this.asyncStorage = config.asyncStorage;\n this.timer = config.timer;\n this.onErrorCb = config.onError;\n\n if (this.asyncStorage) {\n this.strategy = new AsyncPersistStrategy(this);\n } else {\n this.strategy = new SyncPersistStrategy(this);\n }\n }\n\n // --- PersistStrategyContext implementation ---\n\n getStorageKey() {\n return this.storageKey;\n }\n getStorage() {\n return this.storage;\n }\n getAsyncStorage() {\n return this.asyncStorage;\n }\n getTimer() {\n return this.timer;\n }\n getThrottleMs() {\n return this.persistThrottleMs;\n }\n getDebounceMs() {\n return this.persistDebounceMs;\n }\n getGraphAndBloom() {\n if (!this.graphInstance || !this.bloomInstance) return null;\n return { graph: this.graphInstance, bloom: this.bloomInstance };\n }\n isClosed() {\n return this.isClosedFlag;\n }\n isDirty() {\n return this.isDirtyFlag;\n }\n clearDirty() {\n this.isDirtyFlag = false;\n }\n setEngineHealth(health: PassiveIntentTelemetry['engineHealth']) {\n this.engineHealthInternal = health;\n }\n reportError(code: PassiveIntentError['code'], message: string, err: unknown) {\n if (this.onErrorCb) {\n this.onErrorCb({ code, message, originalError: err });\n }\n }\n\n get engineHealth(): PassiveIntentTelemetry['engineHealth'] {\n return this.engineHealthInternal;\n }\n\n /**\n * Provide the live graph and bloom filter after they have been constructed.\n * Must be called before the first `persist()`.\n */\n attach(graph: MarkovGraph, bloom: BloomFilter): void {\n this.graphInstance = graph;\n this.bloomInstance = bloom;\n }\n\n /* ================================================================== */\n /* Restore */\n /* ================================================================== */\n\n /**\n * Attempt to read and deserialise a previously persisted payload.\n * Returns `null` on first run (no stored data) or if the data is corrupt.\n * Never throws — all errors go to `onError`.\n */\n restore(graphConfig: MarkovGraphConfig): { bloom: BloomFilter; graph: MarkovGraph } | null {\n let raw: string | null;\n try {\n raw = this.storage.getItem(this.storageKey);\n } catch (err) {\n if (this.onErrorCb) {\n this.onErrorCb({\n code: 'STORAGE_READ',\n message: err instanceof Error ? err.message : String(err),\n originalError: err,\n });\n }\n return null;\n }\n\n if (!raw) return null;\n\n try {\n const parsed = JSON.parse(raw) as PersistedPayload;\n\n let graph: MarkovGraph;\n if (parsed.graphBinary) {\n const bytes = base64ToUint8(parsed.graphBinary);\n graph = MarkovGraphClass.fromBinary(bytes, graphConfig);\n } else if (parsed.graph) {\n // Legacy JSON format — predates the binary codec.\n graph = MarkovGraphClass.fromJSON(parsed.graph, graphConfig);\n } else {\n return null;\n }\n\n const bloom = parsed.bloomBase64\n ? BloomFilterClass.fromBase64(parsed.bloomBase64)\n : new BloomFilterClass();\n\n return { bloom, graph };\n } catch (err) {\n if (this.onErrorCb) {\n const payloadByteLength =\n typeof TextEncoder !== 'undefined'\n ? new TextEncoder().encode(raw as string).length\n : (raw as string).length;\n this.onErrorCb({\n code: 'RESTORE_PARSE',\n message: err instanceof Error ? err.message : String(err),\n originalError: { cause: err, payloadLength: payloadByteLength },\n });\n }\n return null;\n }\n }\n\n persist(): void {\n this.strategy.persist();\n }\n flushNow(): void {\n this.strategy.flushNow();\n }\n close(): void {\n this.isClosedFlag = true;\n this.strategy.close();\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport { BrowserLifecycleAdapter } from '../adapters.js';\nimport type { LifecycleAdapter, TimerAdapter, TimerHandle } from '../adapters.js';\nimport { EventEmitter } from './event-emitter.js';\nimport type { IntentEventMap } from '../types/events.js';\nimport {\n ATTENTION_RETURN_THRESHOLD_MS,\n MAX_PLAUSIBLE_DWELL_MS,\n USER_IDLE_THRESHOLD_MS,\n} from './constants.js';\n\n/**\n * Configuration for LifecycleCoordinator.\n *\n * All callbacks are supplied by IntentManager and run synchronously\n * inside the onResume handler — no closure captures of coordinator internals.\n */\nexport interface LifecycleCoordinatorConfig {\n /**\n * Optional pre-built adapter. When `undefined` (not set), a new\n * `BrowserLifecycleAdapter` is created internally (and owned). Explicitly\n * passing `null` disables lifecycle tracking entirely.\n *\n * The `undefined` vs. `null` distinction is intentional and mirrors the\n * original `IntentManager` constructor behaviour.\n */\n lifecycleAdapter?: LifecycleAdapter | null;\n timer: TimerAdapter;\n dwellTimeEnabled: boolean;\n emitter: EventEmitter<IntentEventMap>;\n /**\n * Called on resume when the hidden duration was within the plausible range.\n * IntentManager should execute: `previousStateEnteredAt += delta`.\n */\n onAdjustBaseline: (delta: number) => void;\n /**\n * Called on resume when the hidden duration exceeded `MAX_PLAUSIBLE_DWELL_MS`.\n * IntentManager should execute: `previousStateEnteredAt = timer.now()`.\n */\n onResetBaseline: () => void;\n /**\n * Returns whether the engine has an active dwell epoch in progress.\n * Translates to `previousState !== null` in IntentManager.\n */\n hasPreviousState: () => boolean;\n /**\n * Returns the current state the user is viewing, or `null` when no state\n * has been entered yet. Used by the comparison-shopper detection path.\n */\n getPreviousState: () => string | null;\n /**\n * Called when exit intent is detected and Markov math confirms a\n * likely continuation path. Supplied by `IntentManager` which performs\n * the probability check and emits the `exit_intent` event.\n *\n * Optional — when absent, exit-intent detection is disabled even if the\n * underlying `LifecycleAdapter` supports `onExitIntent`.\n */\n onExitIntent?: () => void;\n}\n\n/**\n * LifecycleCoordinator — owns pause/resume handling and session-stale detection.\n *\n * Responsibilities:\n * - Creates or accepts an injected `LifecycleAdapter`\n * - On tab hide (pause): records `tabHiddenAt`\n * - On tab show (resume):\n * - Computes hidden duration\n * - When within plausible range: calls `onAdjustBaseline(duration)` to\n * offset `previousStateEnteredAt` so the dwell clock ignores off-screen time\n * - When duration exceeds `MAX_PLAUSIBLE_DWELL_MS` (OS suspend / hibernate):\n * calls `onResetBaseline()` and emits `session_stale`\n * - Both paths are skipped when `dwellTimeEnabled` is `false`\n * - `destroy()`: deregisters callbacks and (conditionally) tears down the adapter\n * - Idle detection (when the adapter supports `onInteraction`):\n * - Resets a `USER_IDLE_THRESHOLD_MS` debounce timer on every interaction\n * - Emits `user_idle` when the timer fires (no interaction for that duration)\n * - Emits `user_resumed` on the first interaction after an idle period\n * - Adjusts the dwell baseline to exclude idle duration\n */\nexport class LifecycleCoordinator {\n private readonly timer: TimerAdapter;\n private readonly dwellTimeEnabled: boolean;\n private readonly emitter: EventEmitter<IntentEventMap>;\n private readonly onAdjustBaseline: (delta: number) => void;\n private readonly onResetBaseline: () => void;\n private readonly hasPreviousState: () => boolean;\n private readonly getPreviousState: () => string | null;\n\n private lifecycleAdapter: LifecycleAdapter | null;\n private ownsLifecycleAdapter: boolean;\n private pauseUnsub: (() => void) | null = null;\n private resumeUnsub: (() => void) | null = null;\n private exitIntentUnsub: (() => void) | null = null;\n\n /** Timestamp when the tab last became hidden; `null` while visible. */\n private tabHiddenAt: number | null = null;\n\n /* ── Idle-state detection ───────────────────────────────────────────── */\n private lastInteractionAt: number;\n private idleStartedAt: number = 0;\n private isIdle: boolean = false;\n private idleCheckTimer: TimerHandle | null = null;\n private interactionUnsub: (() => void) | null = null;\n\n private readonly onExitIntentCallback: (() => void) | undefined;\n\n constructor(config: LifecycleCoordinatorConfig) {\n this.timer = config.timer;\n this.dwellTimeEnabled = config.dwellTimeEnabled;\n this.emitter = config.emitter;\n this.onAdjustBaseline = config.onAdjustBaseline;\n this.onResetBaseline = config.onResetBaseline;\n this.hasPreviousState = config.hasPreviousState;\n this.getPreviousState = config.getPreviousState;\n this.lastInteractionAt = this.timer.now();\n\n this.onExitIntentCallback = config.onExitIntent;\n\n // Preserve the exact undefined-vs-null semantics from the original constructor:\n // - undefined → create a BrowserLifecycleAdapter (owned by this coordinator)\n // - null → no adapter; lifecycle tracking disabled\n // - object → injected adapter; this coordinator does NOT own it\n if (config.lifecycleAdapter !== undefined) {\n this.lifecycleAdapter = config.lifecycleAdapter;\n this.ownsLifecycleAdapter = false;\n } else {\n this.lifecycleAdapter = typeof window !== 'undefined' ? new BrowserLifecycleAdapter() : null;\n this.ownsLifecycleAdapter = true;\n }\n\n this.bindAdapter(this.lifecycleAdapter);\n }\n\n /**\n * Unsubscribe any existing pause/resume callbacks and re-register them on\n * `adapter` (or leave them null when `adapter` is null).\n *\n * Single source of truth for handler registration — used by both the\n * constructor and `setAdapterForTest`.\n */\n private bindAdapter(adapter: LifecycleAdapter | null): void {\n this.pauseUnsub?.();\n this.pauseUnsub = null;\n this.resumeUnsub?.();\n this.resumeUnsub = null;\n this.exitIntentUnsub?.();\n this.exitIntentUnsub = null;\n this.stopIdleTracking();\n\n if (!adapter) return;\n\n this.pauseUnsub = adapter.onPause(() => {\n this.tabHiddenAt = this.timer.now();\n });\n\n this.resumeUnsub = adapter.onResume(() => {\n if (this.tabHiddenAt === null) return;\n const hiddenDuration = this.timer.now() - this.tabHiddenAt;\n this.tabHiddenAt = null;\n\n // ── Comparison-shopper detection (attention_return) ─────────────────\n // When the user returns after ≥15 s and was viewing a known state,\n // emit an attention_return event so the host app can surface a\n // \"Welcome Back\" experience (e.g. discount modal).\n if (hiddenDuration >= ATTENTION_RETURN_THRESHOLD_MS) {\n const currentState = this.getPreviousState();\n if (currentState !== null) {\n this.emitter.emit('attention_return', {\n state: currentState,\n hiddenDuration,\n });\n }\n }\n\n // All resume-path operations are dwell-time bookkeeping. When dwell-time\n // detection is disabled the caller has opted out of all dwell diagnostics.\n if (!this.dwellTimeEnabled) return;\n\n if (hiddenDuration > MAX_PLAUSIBLE_DWELL_MS) {\n // Almost certainly an OS suspend / hibernate event. Only act when an\n // active dwell epoch is in progress.\n if (this.hasPreviousState()) {\n this.onResetBaseline();\n this.emitter.emit('session_stale', {\n reason: 'hidden_duration_exceeded',\n measuredMs: hiddenDuration,\n thresholdMs: MAX_PLAUSIBLE_DWELL_MS,\n });\n }\n return;\n }\n\n // Offset the dwell baseline only when a state has been entered.\n if (this.hasPreviousState()) {\n this.onAdjustBaseline(hiddenDuration);\n }\n });\n\n this.startIdleTracking(adapter);\n\n // Subscribe to exit-intent detection when the adapter supports it and the\n // coordinator has been given a callback to invoke.\n if (this.onExitIntentCallback !== undefined && typeof adapter.onExitIntent === 'function') {\n this.exitIntentUnsub = adapter.onExitIntent(() => {\n this.onExitIntentCallback!();\n });\n }\n }\n\n /**\n * @internal — test-only hook. Replaces the active lifecycle adapter after\n * construction: unsubscribes the coordinator's pause/resume callbacks from\n * the previous adapter, swaps in the new one, and re-registers the same\n * handlers on the new adapter (or leaves them null when `adapter` is null).\n *\n * Do NOT call this in production code; injecting the adapter via\n * `LifecycleCoordinatorConfig.lifecycleAdapter` is the correct approach.\n */\n /* @internal */\n setAdapterForTest(adapter: LifecycleAdapter | null, owns: boolean): void {\n if (this.ownsLifecycleAdapter) {\n this.lifecycleAdapter?.destroy();\n }\n this.lifecycleAdapter = adapter;\n this.ownsLifecycleAdapter = owns;\n this.tabHiddenAt = null;\n this.isIdle = false;\n this.lastInteractionAt = this.timer.now();\n this.exitIntentUnsub?.();\n this.exitIntentUnsub = null;\n this.bindAdapter(adapter);\n }\n\n /**\n * Deregister this instance's specific pause/resume callbacks and, if this\n * coordinator owns its adapter, destroy it.\n *\n * Injected (shared) adapters are NOT destroyed here — they may serve other\n * `IntentManager` instances and must be torn down by their owner.\n */\n destroy(): void {\n this.stopIdleTracking();\n this.pauseUnsub?.();\n this.pauseUnsub = null;\n this.resumeUnsub?.();\n this.resumeUnsub = null;\n this.exitIntentUnsub?.();\n this.exitIntentUnsub = null;\n if (this.ownsLifecycleAdapter) {\n this.lifecycleAdapter?.destroy();\n }\n }\n\n /* ── Idle-state detection internals ────────────────────────────────── */\n\n /**\n * Set up the interaction subscription and debounce-based idle detection.\n * Gracefully skips when the adapter does not implement `onInteraction`.\n *\n * Every interaction resets a `USER_IDLE_THRESHOLD_MS` one-shot timer.\n * When the timer fires without being cancelled, the user is considered idle.\n */\n private startIdleTracking(adapter: LifecycleAdapter | null): void {\n if (!adapter || typeof adapter.onInteraction !== 'function') return;\n\n const armIdleTimer = (): void => {\n if (this.idleCheckTimer !== null) {\n this.timer.clearTimeout(this.idleCheckTimer);\n }\n this.idleCheckTimer = this.timer.setTimeout(() => {\n this.idleCheckTimer = null;\n if (this.isIdle || !this.hasPreviousState()) return;\n\n this.isIdle = true;\n this.idleStartedAt = this.lastInteractionAt + USER_IDLE_THRESHOLD_MS;\n\n const currentState = this.getPreviousState();\n if (currentState !== null) {\n this.emitter.emit('user_idle', {\n state: currentState,\n idleMs: this.timer.now() - this.idleStartedAt,\n });\n }\n }, USER_IDLE_THRESHOLD_MS);\n };\n\n const unsub = adapter.onInteraction(() => {\n this.lastInteractionAt = this.timer.now();\n\n if (this.isIdle) {\n const idleMs = this.timer.now() - this.idleStartedAt;\n this.isIdle = false;\n\n // Exclude the idle period from the dwell clock.\n if (this.dwellTimeEnabled && this.hasPreviousState()) {\n this.onAdjustBaseline(idleMs);\n }\n\n const currentState = this.getPreviousState();\n if (currentState !== null) {\n this.emitter.emit('user_resumed', {\n state: currentState,\n idleMs,\n });\n }\n }\n\n armIdleTimer();\n });\n\n // The adapter returned null — it cannot deliver interaction events\n // (e.g. SSR / Node.js with a stubbed window). Skip idle tracking.\n if (!unsub) return;\n\n this.interactionUnsub = unsub;\n // Arm the initial timer so idle is detected even with no interactions.\n armIdleTimer();\n }\n\n /** Tear down the interaction listener and idle-check timer. */\n private stopIdleTracking(): void {\n this.interactionUnsub?.();\n this.interactionUnsub = null;\n if (this.idleCheckTimer !== null) {\n this.timer.clearTimeout(this.idleCheckTimer);\n this.idleCheckTimer = null;\n }\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport type { EnginePolicy, PolicyTrackContext } from './engine-policy.js';\nimport type { EventEmitter } from '../event-emitter.js';\nimport type { IntentEventMap } from '../../types/events.js';\nimport { MAX_PLAUSIBLE_DWELL_MS } from '../constants.js';\n\n/**\n * Dependencies injected by IntentManager at construction time.\n */\nexport interface DwellTimePolicyConfig {\n /** Returns `true` when the EntropyGuard flags the session as a bot. */\n isSuspected: () => boolean;\n /** Delegate — forwards the measured dwell to SignalEngine's Welford accumulator. */\n evaluateDwellTime: (state: string, dwellMs: number) => void;\n /** Returns the timestamp when the previous state was entered. */\n getPreviousStateEnteredAt: () => number;\n /** Shared event emitter for `session_stale` emission. */\n emitter: EventEmitter<IntentEventMap>;\n}\n\n/**\n * DwellTimePolicy — gates and measures dwell-time on each state transition.\n *\n * Replaces the inline `if (this.dwellTimeEnabled && …)` conditional that was\n * previously in `IntentManager.runTransitionContextStage`. When this policy\n * is **not** instantiated (because `dwellTime.enabled` is `false`), no\n * dwell-time logic executes at all.\n *\n * Responsibilities:\n * - Measure elapsed time since the previous state was entered.\n * - Guard against implausibly large dwell durations caused by OS suspend\n * (emits `session_stale` with `reason: 'dwell_exceeded'`).\n * - Delegate valid measurements to `SignalEngine.evaluateDwellTime()`.\n * - Skip evaluation when the session is flagged as a suspected bot.\n */\nexport class DwellTimePolicy implements EnginePolicy {\n private readonly isSuspected: () => boolean;\n private readonly evaluateDwellTime: (state: string, dwellMs: number) => void;\n private readonly getPreviousStateEnteredAt: () => number;\n private readonly emitter: EventEmitter<IntentEventMap>;\n\n constructor(config: DwellTimePolicyConfig) {\n this.isSuspected = config.isSuspected;\n this.evaluateDwellTime = config.evaluateDwellTime;\n this.getPreviousStateEnteredAt = config.getPreviousStateEnteredAt;\n this.emitter = config.emitter;\n }\n\n onTrackContext(ctx: PolicyTrackContext): void {\n if (!ctx.from || this.isSuspected()) return;\n\n const dwellMs = ctx.now - this.getPreviousStateEnteredAt();\n\n if (dwellMs > MAX_PLAUSIBLE_DWELL_MS) {\n // Implausibly large dwell — CPU suspend or OS hibernation slipped past\n // the LifecycleAdapter. Discard the measurement and emit a diagnostic\n // event.\n //\n // CONTRACT: IntentManager.runTransitionContextStage unconditionally sets\n // `previousStateEnteredAt = ctx.now` immediately after all onTrackContext\n // hooks complete (including this one), so the stale baseline is always\n // cleared regardless of which branch executes here. If that ordering\n // ever changes, this branch must perform the reset explicitly.\n this.emitter.emit('session_stale', {\n reason: 'dwell_exceeded',\n measuredMs: dwellMs,\n thresholdMs: MAX_PLAUSIBLE_DWELL_MS,\n });\n } else {\n this.evaluateDwellTime(ctx.from, dwellMs);\n }\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport type { EnginePolicy } from './engine-policy.js';\nimport type { MarkovGraph } from '../../core/markov.js';\n\n/**\n * BigramPolicy — records second-order (bigram) Markov transitions.\n *\n * Replaces the inline `if (this.enableBigrams && trajectory.length >= 3)`\n * conditional that was previously in `SignalEngine.recordTransition`.\n * When this policy is **not** instantiated (because `enableBigrams` is\n * `false`), no bigram accounting executes at all.\n *\n * Bigram states are encoded as `\"prev→from\"` → `\"from→to\"` using U+2192\n * as a collision-resistant separator that will not appear in normal URL-based\n * state labels.\n *\n * The frequency-threshold guard prevents sparse bigram pollution during the\n * early learning phase: bigrams are only recorded when the *unigram* source\n * state (`from`) has accumulated at least `bigramFrequencyThreshold`\n * outgoing transitions.\n */\nexport class BigramPolicy implements EnginePolicy {\n private readonly graph: MarkovGraph;\n private readonly bigramFrequencyThreshold: number;\n\n constructor(graph: MarkovGraph, bigramFrequencyThreshold: number) {\n this.graph = graph;\n this.bigramFrequencyThreshold = bigramFrequencyThreshold;\n }\n\n onTransition(from: string, to: string, trajectory: readonly string[]): void {\n if (trajectory.length < 3) return;\n\n const prev2 = trajectory[trajectory.length - 3];\n const bigramFrom = `${prev2}\\u2192${from}`;\n const bigramTo = `${from}\\u2192${to}`;\n\n // Only record when the unigram source has enough outgoing transitions.\n if (this.graph.rowTotal(from) >= this.bigramFrequencyThreshold) {\n this.graph.incrementTransition(bigramFrom, bigramTo);\n }\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport type { EnginePolicy } from './engine-policy.js';\nimport type { PassiveIntentTelemetry } from '../../types/events.js';\n\n/**\n * DriftProtectionPolicy — failsafe killswitch that silences trajectory\n * evaluation when the anomaly rate exceeds a configurable threshold.\n *\n * Replaces the following state that was previously scattered across\n * `SignalEngine`:\n * - `isBaselineDriftedInternal` flag\n * - `driftWindowStart`, `driftWindowTrackCount`, `driftWindowAnomalyCount`\n * - `advanceDriftWindow(now)` method\n * - Drift anomaly counting inside `evaluateTrajectory()`\n * - `baselineStatus` / `isBaselineDrifted` getters\n *\n * Lifecycle:\n * 1. `onTrackStart(now)` is called once per `track()` to advance the\n * rolling evaluation window.\n * 2. `SignalEngine.evaluateTrajectory()` checks `isDrifted` to decide\n * whether to skip evaluation, and calls `recordAnomaly()` when a\n * trajectory anomaly is detected.\n */\nexport class DriftProtectionPolicy implements EnginePolicy {\n private readonly maxAnomalyRate: number;\n private readonly evaluationWindowMs: number;\n\n private drifted = false;\n private windowStart = 0;\n private windowTrackCount = 0;\n private windowAnomalyCount = 0;\n\n constructor(maxAnomalyRate: number, evaluationWindowMs: number) {\n this.maxAnomalyRate = maxAnomalyRate;\n this.evaluationWindowMs = evaluationWindowMs;\n }\n\n /* ── EnginePolicy hook ──────────────────────────────────────────────── */\n\n /**\n * Advance the rolling evaluation window. Resets counters when the window\n * elapses. O(1), no allocations.\n */\n onTrackStart(now: number): void {\n if (now - this.windowStart >= this.evaluationWindowMs) {\n this.windowStart = now;\n this.windowTrackCount = 0;\n this.windowAnomalyCount = 0;\n }\n this.windowTrackCount += 1;\n }\n\n /* ── Queried by SignalEngine ────────────────────────────────────────── */\n\n /** `true` once the anomaly rate has breached the threshold in any window. */\n get isDrifted(): boolean {\n return this.drifted;\n }\n\n /** Telemetry-friendly status label. */\n get baselineStatus(): PassiveIntentTelemetry['baselineStatus'] {\n return this.drifted ? 'drifted' : 'active';\n }\n\n /**\n * Called by `SignalEngine.evaluateTrajectory()` each time a trajectory\n * anomaly is detected (regardless of cooldown gating). Increments the\n * window anomaly counter and flips the `drifted` flag when the rate\n * exceeds `maxAnomalyRate`.\n */\n recordAnomaly(): void {\n this.windowAnomalyCount += 1;\n if (\n this.windowTrackCount > 0 &&\n this.windowAnomalyCount / this.windowTrackCount > this.maxAnomalyRate\n ) {\n this.drifted = true;\n }\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport type { BloomFilter } from '../core/bloom.js';\nimport type { MarkovGraph } from '../core/markov.js';\n\n/**\n * Hard upper bound on each state label accepted over the broadcast channel.\n *\n * A compromised tab could send arbitrarily long strings to exhaust memory in\n * every other tab (heap amplification / buffer-overflow equivalent in JS).\n * Payloads that exceed this limit are **silently dropped** so that a single\n * bad tab cannot degrade the rest of the session fleet.\n */\nexport const MAX_STATE_LENGTH = 256;\n\n/**\n * Wire format of a Markov-transition sync message.\n */\ninterface TransitionMessage {\n /** Discriminant. */\n type: 'transition';\n /** The state the user navigated *from*. */\n from: string;\n /** The state the user navigated *to*. */\n to: string;\n}\n\n/**\n * Wire format of a deterministic counter increment sync message.\n *\n * Only positive, finite `by` values are acted on; malformed or unbounded\n * payloads are silently dropped.\n */\ninterface CounterMessage {\n /** Discriminant. */\n type: 'counter';\n /** Counter key — same length constraints as state labels (≤ MAX_STATE_LENGTH). */\n key: string;\n /** Amount to add to the counter. Must be finite; validated before use. */\n by: number;\n}\n\n/** Union of all wire-protocol message types. */\ntype SyncMessage = TransitionMessage | CounterMessage;\n\n/**\n * Type-guard for `TransitionMessage`.\n *\n * Rejects the payload if `from` or `to` exceed `MAX_STATE_LENGTH` (memory-\n * exhaustion / model-poisoning guard).\n */\nfunction isValidTransitionMessage(msg: Record<string, unknown>): boolean {\n if (msg['type'] !== 'transition') return false;\n if (typeof msg['from'] !== 'string' || typeof msg['to'] !== 'string') return false;\n if (msg['from'].length === 0 || msg['to'].length === 0) return false;\n if (msg['from'].length > MAX_STATE_LENGTH || msg['to'].length > MAX_STATE_LENGTH) return false;\n return true;\n}\n\n/**\n * Type-guard for `CounterMessage`.\n *\n * Rejects the payload if:\n * - `key` is not a non-empty string ≤ `MAX_STATE_LENGTH`\n * - `by` is not a finite number (guards against `Infinity`, `NaN`)\n */\nfunction isValidCounterMessage(msg: Record<string, unknown>): boolean {\n if (msg['type'] !== 'counter') return false;\n if (typeof msg['key'] !== 'string') return false;\n if (msg['key'].length === 0 || msg['key'].length > MAX_STATE_LENGTH) return false;\n if (typeof msg['by'] !== 'number' || !Number.isFinite(msg['by'])) return false;\n return true;\n}\n\n/**\n * Optional cross-tab synchronization layer using the\n * [BroadcastChannel API](https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel).\n *\n * **Design goals**\n *\n * 1. **Model consistency** — when a user navigates in Tab A, Tab B's Markov\n * graph and Bloom filter learn that transition too, so prefetch hints stay\n * accurate across a multi-tab session.\n *\n * 2. **Counter consistency** — when a user increments a named counter in Tab A\n * (e.g. `incrementCounter('articles_read')`), all other tabs receive the\n * same increment so their local counts stay in sync.\n *\n * 3. **XSS / model-poisoning hardening** — every incoming message is\n * validated before touching any data structure: state labels and counter\n * keys must be non-empty strings ≤ `MAX_STATE_LENGTH` (256 chars);\n * counter `by` values must be finite numbers.\n * Oversized or malformed payloads are silently dropped.\n *\n * 4. **Infinite-loop prevention** — remote transitions/increments applied via\n * `applyRemote()` / `applyRemoteCounter()` update in-memory state directly\n * **without** re-broadcasting to the channel.\n *\n * 5. **Bot flood containment** — `IntentManager` only calls `broadcast()` and\n * `broadcastCounter()` when the local `EntropyGuard` has *not* flagged the\n * session as a bot.\n *\n * **SSR / non-browser safety** — `BroadcastSync` checks for `BroadcastChannel`\n * availability at construction time. When it is absent (Node.js, older\n * browsers) the instance is created but all methods become no-ops; `isActive`\n * returns `false` so callers can inspect the state.\n *\n * @example\n * ```ts\n * const sync = new BroadcastSync('passiveintent-sync', graph, bloom, counters);\n * // Called by IntentManager after a verified local transition:\n * sync.broadcast('/home', '/products');\n * // Called by IntentManager after a local counter increment:\n * sync.broadcastCounter('articles_read', 1);\n * // Clean up on destroy:\n * sync.close();\n * ```\n */\nexport class BroadcastSync {\n private readonly channel: BroadcastChannel | null;\n private readonly graph: MarkovGraph;\n private readonly bloom: BloomFilter;\n private readonly counters: Map<string, number>;\n\n /**\n * `true` when a real `BroadcastChannel` was opened successfully.\n * `false` in SSR / environments without the API.\n */\n readonly isActive: boolean;\n\n constructor(\n channelName: string,\n graph: MarkovGraph,\n bloom: BloomFilter,\n counters: Map<string, number> = new Map(),\n ) {\n this.graph = graph;\n this.bloom = bloom;\n this.counters = counters;\n\n if (typeof BroadcastChannel === 'undefined') {\n this.channel = null;\n this.isActive = false;\n return;\n }\n\n this.channel = new BroadcastChannel(channelName);\n this.isActive = true;\n\n this.channel.onmessage = (event: MessageEvent) => {\n this.handleMessage(event.data);\n };\n }\n\n /**\n * Broadcast a locally-verified transition to all other tabs on the same\n * channel.\n *\n * **Must only be called for transitions that have already passed the local\n * `EntropyGuard` check.** `IntentManager` enforces this invariant; do not\n * call this method directly unless you can guarantee the same condition.\n *\n * @param from Normalized source state label (must not be empty).\n * @param to Normalized destination state label (must not be empty).\n */\n broadcast(from: string, to: string): void {\n if (!this.channel) return;\n const msg: TransitionMessage = { type: 'transition', from, to };\n this.channel.postMessage(msg);\n }\n\n /**\n * Broadcast a counter increment to all other tabs so their local counter\n * Maps stay in sync with the tab that originated the increment.\n *\n * **Must only be called when the local `EntropyGuard` has not flagged the\n * session as a bot.** `IntentManager.incrementCounter()` enforces this.\n *\n * @param key Counter key (must not be empty, ≤ MAX_STATE_LENGTH chars).\n * @param by Amount the counter was incremented (must be finite).\n */\n broadcastCounter(key: string, by: number): void {\n if (!this.channel) return;\n const msg: CounterMessage = { type: 'counter', key, by };\n this.channel.postMessage(msg);\n }\n\n /**\n * Apply a validated remote transition to the local in-memory model.\n *\n * This method updates the `MarkovGraph` and `BloomFilter` **without**\n * re-broadcasting the transition, which prevents the infinite-loop amplification\n * that would occur if received transitions were forwarded back to the channel.\n *\n * This is an internal method exposed for testing; production code should rely\n * on `onmessage` calling `handleMessage` automatically.\n *\n * @param from Validated source state label.\n * @param to Validated destination state label.\n */\n applyRemote(from: string, to: string): void {\n this.bloom.add(from);\n this.bloom.add(to);\n this.graph.incrementTransition(from, to);\n }\n\n /**\n * Apply a validated remote counter increment to the local counters Map\n * **without** re-broadcasting, preventing infinite-loop amplification.\n *\n * This is an internal method exposed for testing; production code should rely\n * on `onmessage` calling `handleMessage` automatically.\n *\n * @param key Validated counter key.\n * @param by Validated finite increment amount.\n */\n applyRemoteCounter(key: string, by: number): void {\n if (!this.counters.has(key) && this.counters.size >= 50) return;\n const current = this.counters.get(key) ?? 0;\n this.counters.set(key, current + by);\n }\n\n /**\n * Close the underlying `BroadcastChannel` and release the message handler.\n *\n * Call this inside `IntentManager.destroy()` to prevent ghost listeners.\n * Safe to call multiple times and in non-browser environments.\n */\n close(): void {\n if (!this.channel) return;\n this.channel.onmessage = null;\n this.channel.close();\n }\n\n // ── Private ──────────────────────────────────────────────────────────────\n\n /**\n * Validate and dispatch an incoming raw message from the broadcast channel.\n *\n * Security invariants:\n * - Non-object, null, and unrecognised `type` values are silently dropped.\n * - `TransitionMessage`: `from`/`to` must be non-empty strings ≤ `MAX_STATE_LENGTH`.\n * - `CounterMessage`: `key` must be a non-empty string ≤ `MAX_STATE_LENGTH`;\n * `by` must be a finite number.\n * - No re-broadcasting occurs — remote state is applied locally only.\n */\n private handleMessage(data: unknown): void {\n if (typeof data !== 'object' || data === null) return;\n const msg = data as Record<string, unknown>;\n if (msg['type'] === 'transition') {\n if (!isValidTransitionMessage(msg)) return;\n this.applyRemote(msg['from'] as string, msg['to'] as string);\n } else if (msg['type'] === 'counter') {\n if (!isValidCounterMessage(msg)) return;\n this.applyRemoteCounter(msg['key'] as string, msg['by'] as number);\n }\n // Unknown type values are silently ignored for forward-compatibility.\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport type { EnginePolicy } from './engine-policy.js';\nimport { BroadcastSync } from '../../sync/broadcast-sync.js';\nimport type { BloomFilter } from '../../core/bloom.js';\nimport type { MarkovGraph } from '../../core/markov.js';\n\n/**\n * Configuration for CrossTabSyncPolicy.\n */\nexport interface CrossTabSyncPolicyConfig {\n channelName: string;\n graph: MarkovGraph;\n bloom: BloomFilter;\n counters: Map<string, number>;\n /** Returns `true` when the EntropyGuard flags the session as a bot. */\n isSuspected: () => boolean;\n}\n\n/**\n * CrossTabSyncPolicy — broadcasts locally-verified transitions and counter\n * increments to other tabs via the BroadcastChannel API.\n *\n * Replaces the inline `if (this.broadcastSync && !this.signalEngine.suspected)`\n * conditionals that were previously in `IntentManager.runGraphAndSignalStage`\n * and `IntentManager.incrementCounter`.\n *\n * When this policy is **not** instantiated (because `crossTabSync` is\n * `false`), no broadcast logic executes at all.\n *\n * Security invariants (enforced by the underlying `BroadcastSync`):\n * - Incoming payloads are strictly validated (non-empty, ≤ 256 chars).\n * - Remote transitions are applied without re-broadcasting.\n * - Only non-bot sessions broadcast (guards bot-flood amplification).\n */\nexport class CrossTabSyncPolicy implements EnginePolicy {\n private readonly broadcastSync: BroadcastSync;\n private readonly isSuspected: () => boolean;\n\n constructor(config: CrossTabSyncPolicyConfig) {\n this.broadcastSync = new BroadcastSync(\n config.channelName,\n config.graph,\n config.bloom,\n config.counters,\n );\n this.isSuspected = config.isSuspected;\n }\n\n /**\n * Broadcast a transition after all signal evaluation has completed.\n * Skipped when the session is suspected as a bot.\n */\n onAfterEvaluation(from: string, to: string): void {\n // The `from` parameter name in the hook maps to the transition's source\n // state; `to` is the arriving state. We broadcast the original\n // (from, to) pair.\n // Note: this guard mirrors the original IntentManager conditional exactly.\n if (!this.isSuspected()) {\n this.broadcastSync.broadcast(from, to);\n }\n }\n\n /**\n * Broadcast a counter increment to other tabs.\n * Skipped when the session is suspected as a bot.\n */\n onCounterIncrement(key: string, by: number): void {\n if (!this.isSuspected()) {\n this.broadcastSync.broadcastCounter(key, by);\n }\n }\n\n /**\n * Close the underlying BroadcastChannel and release the message handler.\n */\n destroy(): void {\n this.broadcastSync.close();\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport { BenchmarkRecorder } from '../performance-instrumentation.js';\nimport type { PerformanceReport } from '../performance-instrumentation.js';\nimport { BrowserStorageAdapter, BrowserTimerAdapter } from '../adapters.js';\nimport type { AsyncStorageAdapter, StorageAdapter, TimerAdapter } from '../adapters.js';\nimport { BloomFilter } from '../core/bloom.js';\nimport { MarkovGraph } from '../core/markov.js';\nimport { normalizeRouteState } from '../utils/route-normalizer.js';\nimport type { SerializedMarkovGraph } from '../core/markov.js';\nimport type {\n ConversionPayload,\n PassiveIntentError,\n PassiveIntentTelemetry,\n IntentEventMap,\n IntentManagerConfig,\n} from '../types/events.js';\nimport { MAX_WINDOW_LENGTH } from './constants.js';\nimport { buildIntentManagerOptions } from './config-normalizer.js';\nimport { EventEmitter } from './event-emitter.js';\nimport { SignalEngine } from './signal-engine.js';\nimport { PersistenceCoordinator } from './persistence-coordinator.js';\nimport { LifecycleCoordinator } from './lifecycle-coordinator.js';\nimport type { EnginePolicy } from './policies/engine-policy.js';\nimport { DwellTimePolicy } from './policies/dwell-time-policy.js';\nimport { BigramPolicy } from './policies/bigram-policy.js';\nimport { DriftProtectionPolicy } from './policies/drift-protection-policy.js';\nimport { CrossTabSyncPolicy } from './policies/cross-tab-sync-policy.js';\n\n/**\n * Shared mutable context passed through each `trackStages` pipeline function.\n */\ninterface TrackContext {\n state: string;\n now: number;\n trackStart: number;\n from: string | null;\n isNewToBloom: boolean;\n}\n\n/**\n * Intent manager orchestrates collection + modeling + interventions.\n */\nexport class IntentManager {\n private readonly bloom: BloomFilter;\n private readonly graph: MarkovGraph;\n private readonly baseline: MarkovGraph | null;\n private readonly emitter = new EventEmitter<IntentEventMap>();\n private readonly benchmark: BenchmarkRecorder;\n private readonly timer: TimerAdapter;\n private readonly onError?: (error: PassiveIntentError) => void;\n private readonly botProtection: boolean;\n private readonly stateNormalizer?: (state: string) => string;\n\n /* Pluggable feature policies (deterministic order) */\n private readonly policies: EnginePolicy[];\n\n /* Collaborators */\n private readonly signalEngine: SignalEngine;\n private readonly persistenceCoordinator: PersistenceCoordinator;\n private readonly lifecycleCoordinator: LifecycleCoordinator;\n\n /* Pipeline state */\n private previousState: string | null = null;\n private previousStateEnteredAt: number = 0;\n private recentTrajectory: string[] = [];\n\n /* Deterministic named counters — session-scoped, never persisted */\n private counters = new Map<string, number>();\n\n /* GDPR-compliant telemetry */\n private readonly sessionId: string;\n private readonly assignmentGroup: 'treatment' | 'control';\n\n private readonly trackStages: Array<(ctx: TrackContext) => void>;\n\n constructor(config: IntentManagerConfig = {}) {\n // ── Normalize all config precedence, defaults, and clamping ────────────\n const opts = buildIntentManagerOptions(config);\n\n this.benchmark = new BenchmarkRecorder(config.benchmark);\n this.timer = config.timer ?? new BrowserTimerAdapter();\n this.onError = config.onError;\n this.botProtection = opts.botProtection;\n this.stateNormalizer = config.stateNormalizer;\n\n // Session ID — local-only, never transmitted.\n this.sessionId =\n typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'\n ? crypto.randomUUID()\n : Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);\n\n // A/B holdout assignment: randomly place the session in 'control' or 'treatment'.\n this.assignmentGroup = Math.random() * 100 < opts.holdoutPercent ? 'control' : 'treatment';\n\n // ── PersistenceCoordinator (restore on construction) ──────────────────────\n const persistenceCoordinator = new PersistenceCoordinator({\n storageKey: opts.storageKey,\n persistDebounceMs: opts.persistDebounceMs,\n persistThrottleMs: opts.persistThrottleMs,\n storage: config.storage ?? new BrowserStorageAdapter(),\n asyncStorage: config.asyncStorage ?? null,\n timer: this.timer,\n onError: config.onError,\n });\n this.persistenceCoordinator = persistenceCoordinator;\n\n const restored = persistenceCoordinator.restore(opts.graphConfig);\n\n this.bloom = restored?.bloom ?? new BloomFilter(config.bloom);\n this.graph = restored?.graph ?? new MarkovGraph(opts.graphConfig);\n this.baseline = config.baseline\n ? MarkovGraph.fromJSON(config.baseline, opts.graphConfig)\n : null;\n\n // Attach the live graph + bloom so the coordinator can serialise them.\n persistenceCoordinator.attach(this.graph, this.bloom);\n\n // ── Policies (deterministic order) ─────────────────────────────────────────\n // Each policy is only instantiated when its feature flag is enabled.\n // Policies are called in array order at each hook point.\n const driftPolicy = new DriftProtectionPolicy(\n opts.driftMaxAnomalyRate,\n opts.driftEvaluationWindowMs,\n );\n const policies: EnginePolicy[] = [driftPolicy];\n\n // ── SignalEngine ──────────────────────────────────────────────────────────\n this.signalEngine = new SignalEngine({\n graph: this.graph,\n baseline: this.baseline,\n timer: this.timer,\n benchmark: this.benchmark,\n emitter: this.emitter,\n assignmentGroup: this.assignmentGroup,\n eventCooldownMs: opts.eventCooldownMs,\n dwellTimeMinSamples: opts.dwellTimeMinSamples,\n dwellTimeZScoreThreshold: opts.dwellTimeZScoreThreshold,\n hesitationCorrelationWindowMs: opts.hesitationCorrelationWindowMs,\n trajectorySmoothingEpsilon: opts.trajectorySmoothingEpsilon,\n driftPolicy,\n });\n\n // DwellTimePolicy — only when dwell detection is enabled.\n if (opts.dwellTimeEnabled) {\n policies.push(\n new DwellTimePolicy({\n isSuspected: () => this.signalEngine.suspected,\n evaluateDwellTime: (state, dwellMs) => {\n this.signalEngine.dispatch(this.signalEngine.evaluateDwellTime(state, dwellMs));\n },\n getPreviousStateEnteredAt: () => this.previousStateEnteredAt,\n emitter: this.emitter,\n }),\n );\n }\n\n // BigramPolicy — only when second-order transitions are enabled.\n if (opts.enableBigrams) {\n policies.push(new BigramPolicy(this.graph, opts.bigramFrequencyThreshold));\n }\n\n // CrossTabSyncPolicy — only when cross-tab sync is enabled.\n if (opts.crossTabSync) {\n policies.push(\n new CrossTabSyncPolicy({\n channelName: `passiveintent-sync:${opts.storageKey}`,\n graph: this.graph,\n bloom: this.bloom,\n counters: this.counters,\n isSuspected: () => this.signalEngine.suspected,\n }),\n );\n }\n\n this.policies = policies;\n\n // ── LifecycleCoordinator ──────────────────────────────────────────────────\n this.lifecycleCoordinator = new LifecycleCoordinator({\n lifecycleAdapter: config.lifecycleAdapter,\n timer: this.timer,\n dwellTimeEnabled: opts.dwellTimeEnabled,\n emitter: this.emitter,\n onAdjustBaseline: (delta: number) => {\n this.previousStateEnteredAt += delta;\n },\n onResetBaseline: () => {\n this.previousStateEnteredAt = this.timer.now();\n },\n hasPreviousState: () => this.previousState !== null,\n getPreviousState: () => this.previousState,\n // ── Smart Exit-Intent ──────────────────────────────────────────────────\n // Only fires when the Markov graph indicates a likely continuation path\n // from the current state. The check is performed here (in IntentManager)\n // rather than in LifecycleCoordinator so that the coordinator stays\n // decoupled from graph math.\n onExitIntent: () => {\n if (this.previousState === null) return;\n const candidates = this.graph.getLikelyNextStates(this.previousState, 0.4);\n if (candidates.length === 0) return;\n this.emitter.emit('exit_intent', {\n state: this.previousState,\n likelyNext: candidates[0].state,\n });\n },\n });\n\n // ── Pipeline stages ───────────────────────────────────────────────────────\n // Stages are bound arrow functions so future versions can insert, replace, or\n // reorder steps without touching the core track() loop.\n this.trackStages = [\n this.runBotProtectionStage,\n this.runBloomStage,\n this.runTransitionContextStage,\n this.runGraphAndSignalStage,\n this.runEmitAndPersistStage,\n ];\n }\n\n on<K extends keyof IntentEventMap>(\n event: K,\n listener: (payload: IntentEventMap[K]) => void,\n ): () => void {\n return this.emitter.on(event, listener);\n }\n\n /**\n * Async factory for environments with asynchronous storage backends\n * (React Native AsyncStorage, Capacitor Preferences, IndexedDB wrappers, etc.).\n *\n * `createAsync` awaits the initial `getItem` call to pre-load any persisted\n * Bloom filter + Markov graph **before** constructing the engine, so that\n * the synchronous `track()` hot-path is never blocked by I/O. Once the\n * initial read completes, the engine is instantiated synchronously using\n * a lightweight bridge adapter that vends the pre-read payload to\n * `restore()` — no further synchronous storage access is performed.\n *\n * Subsequent `persist()` calls use `config.asyncStorage.setItem()` in a\n * fire-and-forget manner, guarded by an in-flight write flag to prevent\n * overlapping writes.\n *\n * ```ts\n * const adapter: AsyncStorageAdapter = {\n * getItem: (key) => AsyncStorage.getItem(key),\n * setItem: (key, value) => AsyncStorage.setItem(key, value),\n * };\n * const intent = await IntentManager.createAsync({ asyncStorage: adapter });\n * ```\n *\n * @throws {Error} When `config.asyncStorage` is not provided.\n */\n static async createAsync(config: IntentManagerConfig): Promise<IntentManager> {\n if (!config.asyncStorage) {\n throw new Error('IntentManager.createAsync() requires config.asyncStorage');\n }\n // Use the same default storage key as the config normalizer without\n // incurring a second full normalization pass.\n const storageKey = config.storageKey ?? 'passive-intent';\n // Await the single I/O call up-front so the constructor stays synchronous.\n const raw = await config.asyncStorage.getItem(storageKey);\n\n // Build a minimal sync bridge that serves the pre-read payload to restore()\n // and is otherwise a no-op for setItem (async writes go through asyncStorage\n // inside persist()).\n // Note: `storage` is explicitly omitted from the spread so that if the\n // caller also set `config.storage`, we don't inadvertently trigger the\n // \"both adapters provided\" warning in the constructor.\n const preloadBridge: StorageAdapter = {\n // getItem is invoked exactly once — by restore() in the constructor.\n getItem: () => raw,\n setItem: () => {\n /* writes handled async in persist() */\n },\n };\n const { storage: _omit, ...restConfig } = config;\n return new IntentManager({ ...restConfig, storage: preloadBridge });\n }\n\n /**\n * Track a page view or custom state transition.\n *\n * The `state` argument is automatically normalized via `normalizeRouteState()`\n * before any processing. This means you can pass raw URL strings directly —\n * query strings, hash fragments, trailing slashes, UUIDs, numeric IDs and MongoDB\n * ObjectIDs are all stripped or replaced so the engine always receives a\n * stable, canonical state label.\n *\n * ```ts\n * intent.track('/users/550e8400-e29b-41d4-a716-446655440000/profile?tab=bio');\n * // internally treated as: '/users/:id/profile'\n * ```\n */\n track(state: string): void {\n // Normalise first: strip query strings, hash fragments, trailing slashes,\n // and replace dynamic ID segments (UUIDs, MongoDB ObjectIDs, numeric IDs) with ':id'.\n state = normalizeRouteState(state);\n\n // Apply optional custom normalizer (e.g. for SEO slugs).\n if (this.stateNormalizer) {\n try {\n const normalized = this.stateNormalizer(state);\n const coerced = String(normalized);\n // Empty string is a deliberate \"skip this state\" signal from the\n // normalizer — drop silently without firing a VALIDATION error.\n if (coerced === '') return;\n state = coerced;\n } catch (err) {\n if (this.onError) {\n this.onError({\n code: 'VALIDATION',\n message: `IntentManager.track(): stateNormalizer threw: ${err instanceof Error ? err.message : String(err)}`,\n });\n }\n return;\n }\n }\n\n // Guard: '' is reserved internally as a tombstone marker.\n // Silently drop and surface a non-fatal error rather than crashing the host.\n if (state === '') {\n if (this.onError) {\n this.onError({\n code: 'VALIDATION',\n message: 'IntentManager.track(): state label must not be an empty string',\n });\n }\n return;\n }\n\n const now = this.timer.now();\n const trackStart = this.benchmark.now();\n\n // Advance drift-protection rolling window via policy hooks (O(1), no allocations)\n for (let i = 0; i < this.policies.length; i += 1) this.policies[i].onTrackStart?.(now);\n\n const ctx: TrackContext = {\n state,\n now,\n trackStart,\n from: null,\n isNewToBloom: false,\n };\n\n for (let i = 0; i < this.trackStages.length; i += 1) {\n this.trackStages[i](ctx);\n }\n\n this.benchmark.record('track', trackStart);\n }\n\n private runBotProtectionStage = (ctx: TrackContext): void => {\n if (!this.botProtection) return;\n const botResult = this.signalEngine.recordBotCheck(ctx.now);\n if (botResult.transitionedToBot) {\n this.emitter.emit('bot_detected', { state: ctx.state });\n }\n };\n\n private runBloomStage = (ctx: TrackContext): void => {\n ctx.isNewToBloom = !this.bloom.check(ctx.state);\n const bloomAddStart = this.benchmark.now();\n this.bloom.add(ctx.state);\n this.benchmark.record('bloomAdd', bloomAddStart);\n };\n\n private runTransitionContextStage = (ctx: TrackContext): void => {\n ctx.from = this.previousState;\n this.previousState = ctx.state;\n\n // Dwell-time measurement — delegated to DwellTimePolicy when enabled.\n for (let i = 0; i < this.policies.length; i += 1) this.policies[i].onTrackContext?.(ctx);\n\n // CONTRACT: this reset MUST remain unconditional and MUST happen after all\n // onTrackContext hooks. DwellTimePolicy reads previousStateEnteredAt inside\n // its hook and relies on this line to clear any stale baseline afterwards —\n // including the session_stale (dwell_exceeded) code path.\n this.previousStateEnteredAt = ctx.now;\n\n this.recentTrajectory.push(ctx.state);\n if (this.recentTrajectory.length > MAX_WINDOW_LENGTH) this.recentTrajectory.shift();\n };\n\n private runGraphAndSignalStage = (ctx: TrackContext): void => {\n // Skip graph updates when the session is flagged as a bot.\n // This prevents automation / scrapers from poisoning the Markov\n // transition probabilities that drive real-user predictions.\n if (this.botProtection && this.signalEngine.suspected) {\n // Bloom updates from runBloomStage still need to be persisted even when\n // graph writes are skipped. hasSeen() is a public API that should stay\n // consistent across session reloads: if a URL was visited (even during a\n // suspected-bot burst), it should remain \"seen\" after the page reloads.\n // Only the Markov graph is protected from bot poisoning, not bloom membership.\n if (ctx.isNewToBloom) this.persistenceCoordinator.markDirty();\n return;\n }\n\n if (ctx.from) {\n const incrementStart = this.benchmark.now();\n this.graph.incrementTransition(ctx.from, ctx.state);\n this.benchmark.record('incrementTransition', incrementStart);\n\n // Increment transition counter in SignalEngine\n this.signalEngine.recordTransition(ctx.from, ctx.state, this.recentTrajectory);\n\n // Bigram accounting — delegated to BigramPolicy when enabled.\n for (let i = 0; i < this.policies.length; i += 1)\n this.policies[i].onTransition?.(ctx.from, ctx.state, this.recentTrajectory);\n\n this.persistenceCoordinator.markDirty();\n this.signalEngine.dispatch(this.signalEngine.evaluateEntropy(ctx.state));\n this.signalEngine.dispatch(\n this.signalEngine.evaluateTrajectory(ctx.from, ctx.state, this.recentTrajectory),\n );\n\n // Cross-tab broadcast — delegated to CrossTabSyncPolicy when enabled.\n for (let i = 0; i < this.policies.length; i += 1)\n this.policies[i].onAfterEvaluation?.(ctx.from, ctx.state);\n\n return;\n }\n\n if (ctx.isNewToBloom) {\n this.persistenceCoordinator.markDirty();\n }\n };\n\n private runEmitAndPersistStage = (ctx: TrackContext): void => {\n this.emitter.emit('state_change', { from: ctx.from, to: ctx.state });\n // Synchronous persist on every transition — crash-safe against sudden OS\n // process kills where lifecycle events never fire.\n // The dirty-flag short-circuit keeps this a no-op when nothing changed.\n this.persistenceCoordinator.persist();\n };\n\n /**\n * @internal Test-only accessor for the dwell-clock baseline.\n * Not part of the stable public API — prefixed with `_` to signal that.\n */\n get _previousStateEnteredAt(): number {\n return this.previousStateEnteredAt;\n }\n\n hasSeen(state: string): boolean {\n const start = this.benchmark.now();\n const seen = this.bloom.check(state);\n this.benchmark.record('bloomCheck', start);\n return seen;\n }\n\n /**\n * Reset session-specific state for clean evaluation boundaries.\n * Clears the recent trajectory and previous state, but preserves\n * the learned Markov graph and Bloom filter.\n */\n resetSession(): void {\n this.recentTrajectory = [];\n this.previousState = null;\n this.previousStateEnteredAt = 0;\n }\n\n exportGraph(): SerializedMarkovGraph {\n return this.graph.toJSON();\n }\n\n /**\n * Returns the most likely next states from the current (or previous) state,\n * filtered by a minimum probability threshold and an optional sanitize predicate.\n *\n * Designed for **read-only** UI prefetching hints only. This method exposes\n * predictive data from the Markov graph to the host application so it can\n * preload assets or warm caches for the most probable next routes.\n *\n * ⚠ **Security constraint — you MUST provide a `sanitize` function.**\n * Without a sanitize predicate, the returned list may include state-mutating\n * or privacy-sensitive routes such as `/logout`, `/checkout/pay`, or routes\n * that embed PII (e.g. `/users/john.doe/settings`). The sanitize function\n * must return `false` for any such route. Prefetching must **never** trigger\n * state-mutating side effects — treat the results as navigation hints only.\n *\n * ```ts\n * // ✅ Safe usage with a sanitize guard\n * const hints = intent.predictNextStates(0.3, (state) => {\n * const blocked = ['/logout', '/checkout/pay', '/delete-account'];\n * return !blocked.some((b) => state.startsWith(b)) &&\n * !/\\/users\\/[^/]+\\/pii/.test(state);\n * });\n * hints.forEach(({ state, probability }) => prefetch(state));\n * ```\n *\n * @param threshold Minimum probability in [0, 1] for a state to be included.\n * Defaults to `0.3`.\n * @param sanitize Optional predicate that receives each candidate state label\n * and returns `true` to **include** it or `false` to **exclude**\n * it. When omitted all states above the threshold are returned,\n * which is **unsafe** for production use — always supply this.\n * @returns Filtered and sorted `{ state, probability }[]`, descending by\n * probability. Returns an empty array when no previous state is known\n * or no transitions meet the threshold.\n */\n predictNextStates(\n threshold = 0.3,\n sanitize?: (state: string) => boolean,\n ): { state: string; probability: number }[] {\n if (this.previousState === null) return [];\n const candidates = this.graph.getLikelyNextStates(this.previousState, threshold);\n if (!sanitize) return candidates;\n return candidates.filter(({ state }) => sanitize(state));\n }\n\n flushNow(): void {\n this.persistenceCoordinator.flushNow();\n }\n\n /**\n * Tear down the manager: flush any pending state to storage,\n * cancel the debounce timer, and remove all event listeners.\n *\n * Call this in SPA cleanup paths (React `useEffect` teardown,\n * Vue `onUnmounted`, Angular `ngOnDestroy`) to prevent memory\n * leaks from retained listener references.\n *\n * After `destroy()` the instance should be discarded.\n */\n destroy(): void {\n this.persistenceCoordinator.flushNow(); // best-effort final write (may be async)\n this.persistenceCoordinator.close(); // prevent post-destroy timer re-arm\n this.emitter.removeAll();\n this.lifecycleCoordinator.destroy();\n for (let i = 0; i < this.policies.length; i += 1) this.policies[i].destroy?.();\n }\n\n /**\n * Returns a GDPR-compliant telemetry snapshot for the current session.\n *\n * All fields are aggregate counters or derived status flags.\n * No raw behavioral data, no state labels, and no user-identifying\n * information is included. Safe to send to your own analytics endpoint\n * without triggering GDPR personal-data obligations.\n *\n * ```ts\n * const t = intent.getTelemetry();\n * // { sessionId: 'a1b2...', transitionsEvaluated: 42, botStatus: 'human',\n * // anomaliesFired: 3, engineHealth: 'healthy' }\n * ```\n */\n getTelemetry(): PassiveIntentTelemetry {\n return {\n sessionId: this.sessionId,\n transitionsEvaluated: this.signalEngine.transitionsEvaluated,\n botStatus: this.signalEngine.suspected ? 'suspected_bot' : 'human',\n anomaliesFired: this.signalEngine.anomaliesFired,\n engineHealth: this.persistenceCoordinator.engineHealth,\n baselineStatus: this.signalEngine.baselineStatus,\n assignmentGroup: this.assignmentGroup,\n };\n }\n\n /**\n * Record a conversion event and emit it through the event bus.\n *\n * Use this to measure the ROI of intent-driven interventions (e.g.\n * whether a hesitation discount actually led to a purchase).\n *\n * ```ts\n * intent.on('conversion', ({ type, value, currency }) => {\n * // All local — log to your own backend if needed\n * console.log(`Conversion: ${type} ${value} ${currency}`);\n * });\n *\n * // After a purchase completes:\n * intent.trackConversion({ type: 'purchase', value: 49.99, currency: 'USD' });\n * ```\n *\n * **Privacy note:** `type` must not contain user identifiers.\n * This event never leaves the device unless your `conversion` listener\n * explicitly sends it — which remains entirely under your control.\n */\n trackConversion(payload: ConversionPayload): void {\n this.emitter.emit('conversion', payload);\n }\n\n /**\n * Increment a named counter by `by` (default 1) and return the new value.\n *\n * Counters are deterministic: unlike the probabilistic Bloom filter, they\n * track exact counts with no false positives. Use them for business\n * metrics such as \"articles read\", \"items added to cart\", or any case\n * where an exact integer matters.\n *\n * Counters are session-scoped and never persisted to storage.\n *\n * ```ts\n * intent.incrementCounter('articles_read'); // 1\n * intent.incrementCounter('articles_read'); // 2\n * intent.incrementCounter('articles_read', 3); // 5\n * ```\n *\n * @param key - Identifier for the counter. Must not be an empty string.\n * @param by - Amount to add. Defaults to 1. Must be a finite number.\n * @returns The new counter value after incrementing.\n */\n incrementCounter(key: string, by = 1): number {\n if (key === '') {\n if (this.onError) {\n this.onError({\n code: 'VALIDATION',\n message: 'IntentManager.incrementCounter(): key must not be an empty string',\n });\n }\n return 0;\n }\n if (!Number.isFinite(by)) {\n if (this.onError) {\n this.onError({\n code: 'VALIDATION',\n message: `IntentManager.incrementCounter(): 'by' must be a finite number, got ${by}`,\n });\n }\n return this.counters.get(key) ?? 0;\n }\n if (!this.counters.has(key) && this.counters.size >= 50) {\n if (this.onError) {\n this.onError({\n code: 'LIMIT_EXCEEDED',\n message: 'IntentManager.incrementCounter(): max unique counter keys (50) reached',\n });\n }\n return 0;\n }\n const next = (this.counters.get(key) ?? 0) + by;\n this.counters.set(key, next);\n // Broadcast the increment to other tabs via CrossTabSyncPolicy.\n for (let i = 0; i < this.policies.length; i += 1)\n this.policies[i].onCounterIncrement?.(key, by);\n return next;\n }\n\n /**\n * Return the current value of a named counter, or 0 if it has never been\n * incremented.\n *\n * ```ts\n * intent.getCounter('articles_read'); // 0 before any increments\n * ```\n *\n * @param key - Identifier for the counter.\n */\n getCounter(key: string): number {\n return this.counters.get(key) ?? 0;\n }\n\n /**\n * Reset a named counter to 0.\n *\n * After this call `getCounter(key)` returns 0. The counter entry is\n * removed from internal storage rather than being set to 0, keeping the\n * map compact.\n *\n * ```ts\n * intent.incrementCounter('articles_read', 5);\n * intent.resetCounter('articles_read');\n * intent.getCounter('articles_read'); // 0\n * ```\n *\n * @param key - Identifier for the counter to reset.\n */\n resetCounter(key: string): void {\n this.counters.delete(key);\n }\n\n getPerformanceReport(): PerformanceReport {\n const serialized = this.graph.toJSON();\n return this.benchmark.report({\n stateCount: this.graph.stateCount(),\n totalTransitions: this.graph.totalTransitions(),\n bloomBitsetBytes: this.bloom.getBitsetByteSize(),\n serializedGraphBytes: this.benchmark.serializedSizeBytes(serialized),\n });\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport type { IStateModel } from '../types/microkernel.js';\n\n// ---------------------------------------------------------------------------\n// Internal BFS types\n// ---------------------------------------------------------------------------\n\n/**\n * A single node on the BFS frontier during the hitting-probability walk.\n *\n * Declared inline (not a class) so V8 can allocate it cheaply as a plain\n * object literal. The BFS queue is short-lived and GC'd after `updateBaseline`\n * returns, so allocation cost is irrelevant on the hot path.\n */\ninterface BFSNode {\n /** Normalised route label at this position in the walk. */\n readonly state: string;\n /**\n * Product of all edge probabilities from the source state to this node.\n *\n * At the source node this is 1.0 (multiplicative identity). Each hop\n * multiplies by the Markov transition probability P(nextState | state),\n * producing the joint probability of the entire path up to this point.\n */\n readonly pathProb: number;\n /** Number of hops taken from the source state to this node. */\n readonly depth: number;\n /**\n * States already visited on this specific path from the source to this node.\n * Tracked per-path (not globally) so that converging simple paths are not\n * incorrectly pruned: two different paths may visit the same intermediate\n * state independently without forming a cycle.\n */\n readonly pathVisited: ReadonlySet<string>;\n}\n\n// ---------------------------------------------------------------------------\n// PropensityCalculator\n// ---------------------------------------------------------------------------\n\n/**\n * Real-Time Propensity Calculator\n *\n * Produces a single normalised score in [0, 1] answering the question:\n * \"How likely is the current user session to reach `targetState` given\n * the observed behavioural friction so far?\"\n *\n * The score is the product of two independent, orthogonal factors:\n *\n * ─────────────────────────────────────────────────────────────────────\n * Factor 1 — Markov hitting probability (structural, graph-derived)\n * ─────────────────────────────────────────────────────────────────────\n *\n * P_reach = Σ ∏ P(s_{i+1} | s_i)\n * ∀ simple paths currentState → … → targetState\n * of length 1 … maxDepth\n *\n * Computed once by a depth-bounded BFS over the live Markov graph and\n * cached in `cachedBaseline`. Separating this from the Z-score means the\n * hot path (`getRealTimePropensity`) never re-traverses the graph.\n *\n * ─────────────────────────────────────────────────────────────────────\n * Factor 2 — Welford Z-score friction penalty (behavioural, real-time)\n * ─────────────────────────────────────────────────────────────────────\n *\n * frictionPenalty = exp(−α × max(0, z))\n *\n * Derived from the trajectory Z-score emitted by `SignalEngine.evaluateTrajectory`\n * (itself computed via Welford online variance over the log-likelihood series).\n * A high positive z means the current path deviates significantly from the\n * calibrated baseline — the user is confused or frustrated — which reduces\n * the probability of a clean conversion.\n *\n * `max(0, z)` clamps negative Z-scores to zero: a trajectory that is *more*\n * likely than baseline (smooth, decisive navigation) must not inflate the\n * propensity above the raw structural probability.\n *\n * ─────────────────────────────────────────────────────────────────────\n * Combined score\n * ─────────────────────────────────────────────────────────────────────\n *\n * propensity = P_reach × exp(−α × max(0, z))\n *\n * At the default α = 0.2:\n * z = 0 → penalty = 1.000 (no friction — full structural propensity)\n * z = 3.5 → penalty ≈ 0.497 (divergence threshold — score halved)\n * z = 6.9 → penalty ≈ 0.250 (severe anomaly — score quartered)\n *\n * ─────────────────────────────────────────────────────────────────────\n * Performance contract\n * ─────────────────────────────────────────────────────────────────────\n *\n * `getRealTimePropensity()` — throttled to ≤ 1 full computation per\n * THROTTLE_MS (default 500 ms). Throttled calls: zero allocations,\n * one `performance.now()` read, one float comparison, one return.\n * Full computation: one `performance.now()`, one `Math.exp()`, one\n * multiply. Runs comfortably under 1 µs on V8.\n *\n * `updateBaseline()` — O(D × F) BFS where D = maxDepth and F = average\n * graph fan-out. At D = 3, F ≤ 8 this is ≤ 512 frontier nodes.\n * Must NOT be called on every `track()` — only when the\n * (currentState, targetState) pair changes.\n *\n * Zero external dependencies. No network I/O. No PII.\n */\nexport class PropensityCalculator {\n /**\n * Exponential decay sensitivity constant α.\n *\n * Controls how sharply anomalous Z-scores suppress the propensity score.\n *\n * frictionPenalty = exp(−α × z)\n *\n * Default 0.2 is calibrated against the library's default `divergenceThreshold`\n * of 3.5: at that threshold the score is halved, providing a meaningful\n * real-time signal without over-penalising brief navigation anomalies.\n *\n * Increase α (e.g., 0.4) for higher sensitivity in short funnel flows.\n * Decrease α (e.g., 0.1) for longer, noisier browsing sessions.\n */\n private readonly alpha: number;\n\n /**\n * Cached Markov hitting probability from the most recent `updateBaseline()` call.\n *\n * Stores the sum of joint path probabilities over all simple paths of\n * length 1 … maxDepth that connect `currentState` to `targetState` in the\n * live Markov graph. Clamped to [0, 1] by `updateBaseline`.\n *\n * A value of 0 signals one of:\n * (a) `updateBaseline()` has never been called in this session.\n * (b) `targetState` is structurally unreachable from `currentState`\n * within `maxDepth` hops given the observed transition history.\n */\n private cachedBaseline: number;\n\n /**\n * `performance.now()` timestamp of the last accepted propensity computation.\n *\n * Compared against the current timestamp on each `getRealTimePropensity()`\n * call to enforce the THROTTLE_MS gate.\n *\n * Initialized to `-Infinity` so the very first call always satisfies\n * `now - lastCalculationTime ≥ THROTTLE_MS` regardless of what\n * `performance.now()` returns — including 0 in controlled test environments.\n * Using `0` would throttle the first call whenever the clock also starts at 0.\n */\n private lastCalculationTime: number;\n\n /**\n * The propensity score produced by the most recent full (non-throttled) computation.\n *\n * Returned as-is on all subsequent calls within the THROTTLE_MS window,\n * avoiding redundant `Math.exp()` evaluations when the caller polls faster\n * than the throttle interval.\n */\n private lastPropensity: number;\n\n /**\n * Minimum elapsed time in milliseconds between full propensity re-computations.\n *\n * 500 ms was chosen to:\n * • Align with the dwell-time sampling cadence so the score only updates\n * when new dwell-time evidence has likely been collected.\n * • Prevent score oscillation in React consumers: a 500 ms stable window\n * maps to a single React render cycle at typical re-render rates.\n * • Stay well above the p99 `track()` latency (1.6 µs in benchmarks), so\n * throttling never masks a meaningful computation.\n */\n private readonly THROTTLE_MS: number;\n\n constructor(alpha: number = 0.2, throttleMs: number = 500) {\n // Negative alpha inverts the friction relationship (higher z → higher score),\n // and non-finite alpha (NaN or ±Infinity) causes Math.exp to produce NaN at\n // z=0 via Infinity×0. Clamp to [0, ∞) finite; 0 means no friction applied.\n this.alpha = Number.isFinite(alpha) && alpha >= 0 ? alpha : 0;\n this.cachedBaseline = 0;\n this.lastCalculationTime = -Infinity;\n this.lastPropensity = 0;\n // NaN throttleMs disables throttling silently (n < NaN is always false).\n // Infinity throttleMs freezes the score after the first computation forever.\n // Both are hazards; fall back to the documented 500 ms default.\n this.THROTTLE_MS = Number.isFinite(throttleMs) && throttleMs >= 0 ? throttleMs : 500;\n }\n\n // -------------------------------------------------------------------------\n // Public API\n // -------------------------------------------------------------------------\n\n /**\n * Recompute and cache the structural hitting probability via a depth-bounded BFS.\n *\n * ### Why BFS over DFS?\n * BFS explores the graph level-by-level (by hop count), making it natural to\n * accumulate probability mass per depth and terminate cleanly at `maxDepth`.\n * With iterative DFS an explicit depth counter is needed alongside the stack;\n * BFS's queue subsumes both responsibilities and reads more clearly for a\n * probabilistic forward walk.\n *\n * ### Cycle prevention\n * Each queued node carries its own `pathVisited` set — the states already on\n * that specific path from `currentState` to this node. Before enqueuing a\n * neighbor we check `pathVisited` and skip it if it appears, preventing the\n * BFS from following cycles (A→B→A→…) along any single route. Unlike a\n * global visited set, this approach allows two *different* paths to pass\n * through the same intermediate state independently, which is required to\n * correctly sum all simple-path contributions to the hitting probability.\n *\n * ### Probability accumulation\n * Each BFS node carries `pathProb` — the running product of edge probabilities\n * from the source. When a neighbour is `targetState`, we add\n * `pathProb × P(target | state)` to `accumulated` and do NOT enqueue\n * `targetState` for further expansion — once the target is reached, the path\n * has terminated. This correctly handles multiple paths of different lengths\n * that all converge on the target.\n *\n * @param graph The live `IStateModel`. Only `getLikelyNext` is used,\n * so any conforming implementation (real graph, test stub)\n * works without change.\n * @param currentState The user's current observed state (normalised route string).\n * @param targetState The goal state whose reachability we are estimating\n * (e.g., `/checkout/confirm`, `/onboarding/complete`).\n * @param maxDepth Maximum BFS hops. Default 3. At fan-out 8 the frontier\n * is ≤ 8^3 = 512 nodes — safe for a synchronous call.\n * Values above 5 are not recommended: at fan-out 8, depth 5\n * yields 32 768 nodes and adds measurable latency.\n */\n public updateBaseline(\n graph: IStateModel,\n currentState: string,\n targetState: string,\n maxDepth: number = 3,\n ): void {\n // ── Input sanitization ────────────────────────────────────────────────────\n // NaN maxDepth: `depth + 1 < NaN` is always false, so non-target neighbors\n // are never enqueued — only direct target edges are found, silently ignoring\n // the requested depth. Infinity maxDepth removes the depth gate entirely,\n // risking unbounded BFS in dense graphs. Fall back to the documented default.\n const safeMaxDepth = Number.isFinite(maxDepth) && maxDepth >= 1 ? Math.floor(maxDepth) : 3;\n\n // ── Trivial case: the user is already at the target ───────────────────────\n // Markov hitting probability from a state to itself is 1 by definition —\n // the chain has already hit the absorbing target state.\n if (currentState === targetState) {\n this.cachedBaseline = 1;\n return;\n }\n\n // ── BFS initialisation ────────────────────────────────────────────────────\n // The queue holds frontier nodes in FIFO order.\n // `pathVisited` is tracked per-path so that converging simple paths are not\n // incorrectly pruned: two routes may share an intermediate state without\n // forming a cycle. A global visited set would drop valid second arrivals.\n const queue: BFSNode[] = [\n { state: currentState, pathProb: 1, depth: 0, pathVisited: new Set([currentState]) },\n ];\n\n // Running sum of joint path probabilities that terminate at `targetState`.\n let accumulated = 0;\n\n while (queue.length > 0) {\n // `shift()` is O(n) but the queue is bounded by the number of distinct\n // simple paths: at fan-out F and depth D that is at most F×(F-1)^(D-1).\n // At D=3, F=8 this is ~400 entries — a ring-buffer would save nothing.\n const node = queue.shift()!;\n\n // ── Expand: enumerate all outgoing transitions from this state ──────────\n // Threshold 0 returns every observed edge regardless of probability.\n // We intentionally do not filter: even a 1% edge can compound into a\n // meaningful multi-hop path, and silently dropping low-probability edges\n // would understate reachability.\n const neighbours = graph.getLikelyNext(node.state, 0);\n\n for (const { state: nextState, probability: edgeProb } of neighbours) {\n // Skip states already on this path to prevent cycles.\n if (node.pathVisited.has(nextState)) {\n continue;\n }\n\n // Joint probability of the path ending at `nextState`:\n // pathProb(node → nextState) = pathProb(source → node) × P(nextState | node)\n const reachProb = node.pathProb * edgeProb;\n\n if (nextState === targetState) {\n // ── Target reached — accumulate and continue ────────────────────────\n // Do NOT enqueue `targetState`: we only need to count arrivals,\n // not expand from the goal. Multiple paths of different lengths\n // can hit the target, so we accumulate rather than early-return.\n accumulated += reachProb;\n } else if (node.depth + 1 < safeMaxDepth) {\n // ── Not yet at target and depth budget remains — keep walking ────────\n // Push a shallow copy of pathVisited with nextState added so each\n // queued node carries its own independent path history.\n const nextPathVisited = new Set(node.pathVisited);\n nextPathVisited.add(nextState);\n queue.push({\n state: nextState,\n pathProb: reachProb,\n depth: node.depth + 1,\n pathVisited: nextPathVisited,\n });\n }\n // States beyond maxDepth are silently discarded —\n // they cannot improve the estimate without risking cycle inflation.\n }\n }\n\n // Clamp to [0, 1]: by the Markov chain probability axioms, the hitting\n // probability is in [0, 1]. Floating-point products across many parallel\n // paths can accumulate rounding error beyond 1.0 in degenerate graphs;\n // the clamp is a correctness safety net, not the normal code path.\n this.cachedBaseline = Math.min(1, accumulated);\n }\n\n /**\n * Return the real-time propensity score, throttled to at most one full\n * computation per `THROTTLE_MS` milliseconds.\n *\n * ### Formula\n *\n * frictionPenalty = exp(−α × max(0, currentZScore))\n * propensity = cachedBaseline × frictionPenalty\n *\n * ### Why exponential decay for the friction penalty?\n *\n * • It maps every non-negative Z-score to a unique value in (0, 1],\n * preserving the [0, 1] domain without a separate clamp.\n * • It is monotonically decreasing — higher friction always reduces propensity.\n * • It is C∞ differentiable — smooth score transitions eliminate visual\n * jitter in React consumers that read the score via `usePassiveIntent`.\n * • It requires a single `Math.exp()` call — < 100 ns on V8.\n * • The `max(0, z)` clamp ensures negative Z-scores (the user navigates\n * *better* than baseline) produce no friction, keeping the score at\n * `cachedBaseline` rather than inflating it above the Markov probability.\n *\n * ### Throttle mechanics\n *\n * `performance.now()` provides sub-millisecond resolution without OS-level\n * privileges (resolution: ~0.1 ms in secure browser contexts, ~1 µs in Node.js).\n * It is monotonically increasing within a browsing context, immune to wall-clock\n * adjustments that would cause `Date.now()` to produce negative deltas.\n *\n * @param currentZScore Trajectory Z-score from `SignalEngine.evaluateTrajectory`.\n * Pass 0 on cold start or when no baseline is configured.\n * Values < 0 are treated as 0 (no friction for healthy paths).\n * @returns Propensity score in [0, 1]. 0 when no baseline is available.\n */\n public getRealTimePropensity(currentZScore: number): number {\n // ── Throttle gate ──────────────────────────────────────────────────────────\n // performance.now() returns a DOMHighResTimeStamp in milliseconds.\n // Comparing against THROTTLE_MS determines whether we are within the\n // stable window established by the previous full computation.\n const now = performance.now();\n\n if (now - this.lastCalculationTime < this.THROTTLE_MS) {\n // Within the throttle window: return the last score with zero work.\n // This is the overwhelmingly common case during rapid `track()` bursts.\n return this.lastPropensity;\n }\n\n // ── No-baseline early exit ─────────────────────────────────────────────────\n // cachedBaseline === 0 means the target state is unreachable from the\n // current position within maxDepth hops (or updateBaseline has not been\n // called yet). Multiplying zero by any penalty factor yields zero;\n // we skip Math.exp() and return early.\n //\n // We still advance lastCalculationTime so repeated calls on an unreachable\n // target are throttled and do not cause a performance.now() call storm.\n if (this.cachedBaseline <= 0) {\n this.lastCalculationTime = now;\n this.lastPropensity = 0;\n return 0;\n }\n\n // ── Welford Z-score exponential friction penalty ───────────────────────────\n //\n // The Z-score is produced by SignalEngine.evaluateTrajectory using Welford's\n // online algorithm for running mean and variance of the log-likelihood series:\n //\n // z = (LL_observed − μ_baseline) / (σ_baseline × √(W_max / N))\n //\n // where LL_observed is the log-likelihood of the current trajectory under the\n // live graph, μ_baseline and σ_baseline are the Welford-derived mean and\n // standard deviation of the baseline log-likelihood distribution, W_max is the\n // maximum trajectory window length, and N is the current window length.\n //\n // We map this Z-score to a friction multiplier via exponential decay:\n //\n // frictionPenalty = exp(−α × max(0, z))\n //\n // The `max(0, z)` clamp zeroes out any benefit from below-baseline deviation\n // (the user is navigating more efficiently than average — we do not reward\n // this, we simply report no friction).\n // Non-finite z-scores (NaN, ±Infinity) propagate through Math.exp to NaN or\n // produce degenerate results, corrupting lastPropensity.\n // Treat them as 0 (no friction): the caller provided no usable signal.\n const safeZ = Number.isFinite(currentZScore) ? currentZScore : 0;\n const frictionPenalty = Math.exp(-this.alpha * Math.max(0, safeZ));\n\n // ── Combined propensity ────────────────────────────────────────────────────\n //\n // propensity = P_reach(current → target, maxDepth) × exp(−α × max(0, z))\n //\n // This is the Hadamard product of the structural graph estimate and the\n // real-time behavioural signal. Both factors are in [0, 1], so the result\n // is guaranteed to remain in [0, 1] without a final clamp.\n const propensity = this.cachedBaseline * frictionPenalty;\n\n // ── Persist for throttle window ────────────────────────────────────────────\n this.lastPropensity = propensity;\n this.lastCalculationTime = now;\n\n return propensity;\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/**\n * IntentEngine — Layer 2 Microkernel\n * --------------------------------------------------------\n * The raw, platform-agnostic intent detection kernel.\n *\n * This class has ZERO hardcoded references to `window`, `document`, or\n * `localStorage`. Every platform concern is delegated to the four adapter\n * interfaces supplied in `IntentEngineConfig`:\n *\n * - IInputAdapter — push-based navigation events from the host domain\n * - ILifecycleAdapter — pause / resume / exit-intent lifecycle signals\n * - IStateModel — Markov graph + Bloom filter signal evaluation\n * - IPersistenceAdapter — key-value storage I/O\n *\n * Layer 3 (`IntentManager`) wraps this kernel and wires in browser-specific\n * implementations for environments that have a DOM. All existing\n * `IntentManager` behavior is preserved — this class does not replace it.\n */\n\nimport type {\n IntentEngineConfig,\n IInputAdapter,\n ILifecycleAdapter,\n IStateModel,\n IPersistenceAdapter,\n} from '../types/microkernel.js';\nimport type { IntentEventMap } from '../types/events.js';\nimport { EventEmitter } from './event-emitter.js';\nimport { normalizeRouteState } from '../utils/route-normalizer.js';\n\n/** Maximum trajectory window kept for signal evaluation. */\nconst TRAJECTORY_WINDOW = 20;\n\nexport class IntentEngine {\n private readonly emitter = new EventEmitter<IntentEventMap>();\n private readonly stateModel: IStateModel;\n private readonly persistence: IPersistenceAdapter;\n private readonly lifecycle: ILifecycleAdapter;\n private readonly input: IInputAdapter | undefined;\n private readonly storageKey: string;\n private readonly stateNormalizer: ((state: string) => string) | undefined;\n private readonly onError: ((error: { code: string; message: string }) => void) | undefined;\n\n /** Unsubscribe functions collected during construction, released in destroy(). */\n private readonly teardowns: Array<() => void> = [];\n\n /** Most recently tracked state. */\n private previousState: string | null = null;\n /** Rolling window of recently visited states for trajectory evaluation. */\n private recentTrajectory: string[] = [];\n\n constructor(config: IntentEngineConfig) {\n this.stateModel = config.stateModel;\n this.persistence = config.persistence;\n this.lifecycle = config.lifecycle;\n this.input = config.input;\n this.storageKey = config.storageKey ?? 'passive-intent-engine';\n this.stateNormalizer = config.stateNormalizer;\n this.onError = config.onError;\n\n // ── 1. Restore persisted model state ──────────────────────────────────────\n let raw: string | null = null;\n try {\n raw = this.persistence.load(this.storageKey);\n } catch (err) {\n this.onError?.({\n code: 'RESTORE_READ',\n message: `IntentEngine: failed to read persisted state: ${\n err instanceof Error ? err.message : String(err)\n }`,\n });\n }\n if (raw !== null) {\n try {\n this.stateModel.restore(raw);\n } catch (err) {\n this.onError?.({\n code: 'RESTORE_PARSE',\n message: `IntentEngine: failed to restore persisted state: ${\n err instanceof Error ? err.message : String(err)\n }`,\n });\n }\n }\n\n // ── 2. Subscribe to IInputAdapter (push-based navigation) ─────────────────\n if (this.input) {\n try {\n const unsubInput = this.input.subscribe((state) => this._processState(state));\n this.teardowns.push(unsubInput);\n } catch (err) {\n this.onError?.({\n code: 'ADAPTER_SETUP',\n message: `IntentEngine: input.subscribe() threw: ${\n err instanceof Error ? err.message : String(err)\n }`,\n });\n }\n }\n\n // ── 3. Wire ILifecycleAdapter ──────────────────────────────────────────────\n // Persist on pause so state survives app backgrounding / tab hide.\n try {\n this.teardowns.push(\n this.lifecycle.onPause(() => {\n this._persist();\n }),\n );\n } catch (err) {\n this.onError?.({\n code: 'ADAPTER_SETUP',\n message: `IntentEngine: lifecycle.onPause() threw: ${\n err instanceof Error ? err.message : String(err)\n }`,\n });\n }\n\n // Exit-intent: only fire when the graph has a likely continuation path.\n if (typeof this.lifecycle.onExitIntent === 'function') {\n try {\n this.teardowns.push(\n this.lifecycle.onExitIntent(() => {\n if (this.previousState === null) return;\n let candidates: { state: string; probability: number }[] = [];\n try {\n candidates = this.stateModel.getLikelyNext(this.previousState, 0.4);\n } catch (err) {\n this.onError?.({\n code: 'STATE_MODEL',\n message: `IntentEngine: stateModel.getLikelyNext() threw: ${\n err instanceof Error ? err.message : String(err)\n }`,\n });\n }\n if (candidates.length === 0) return;\n this.emitter.emit('exit_intent', {\n state: this.previousState,\n likelyNext: candidates[0].state,\n });\n }),\n );\n } catch (err) {\n this.onError?.({\n code: 'ADAPTER_SETUP',\n message: `IntentEngine: lifecycle.onExitIntent() threw: ${\n err instanceof Error ? err.message : String(err)\n }`,\n });\n }\n }\n }\n\n /* ------------------------------------------------------------------ */\n /* Public API */\n /* ------------------------------------------------------------------ */\n\n /**\n * Subscribe to an intent event.\n *\n * ```ts\n * const off = engine.on('high_entropy', ({ state, normalizedEntropy }) => {\n * console.log(`High entropy in state ${state}: ${normalizedEntropy}`);\n * });\n * // later…\n * off(); // unsubscribe\n * ```\n *\n * @returns An unsubscribe function.\n */\n on<K extends keyof IntentEventMap>(\n event: K,\n listener: (payload: IntentEventMap[K]) => void,\n ): () => void {\n return this.emitter.on(event, listener);\n }\n\n /**\n * Manually track a state transition.\n *\n * Use this when no `IInputAdapter` is provided, or to supplement automatic\n * navigation tracking with custom application events.\n *\n * The state is normalized via `normalizeRouteState()` before processing.\n *\n * ```ts\n * engine.track('/checkout/review');\n * ```\n */\n track(state: string): void {\n this._processState(state);\n }\n\n /**\n * Tear down the engine: flush pending state, unsubscribe all listeners,\n * and release adapter resources.\n *\n * Call this in SPA cleanup paths (React `useEffect` return, Vue\n * `onUnmounted`, Angular `ngOnDestroy`).\n */\n destroy(): void {\n this._persist();\n for (const teardown of this.teardowns) {\n try {\n teardown();\n } catch (err) {\n this.onError?.({\n code: 'ADAPTER_TEARDOWN',\n message: `IntentEngine: teardown threw: ${\n err instanceof Error ? err.message : String(err)\n }`,\n });\n }\n }\n try {\n this.lifecycle.destroy();\n } catch (err) {\n this.onError?.({\n code: 'ADAPTER_TEARDOWN',\n message: `IntentEngine: lifecycle.destroy() threw: ${\n err instanceof Error ? err.message : String(err)\n }`,\n });\n }\n if (this.input) {\n try {\n this.input.destroy();\n } catch (err) {\n this.onError?.({\n code: 'ADAPTER_TEARDOWN',\n message: `IntentEngine: input.destroy() threw: ${\n err instanceof Error ? err.message : String(err)\n }`,\n });\n }\n }\n this.emitter.removeAll();\n }\n\n /* ------------------------------------------------------------------ */\n /* Private pipeline */\n /* ------------------------------------------------------------------ */\n\n /**\n * Core processing pipeline. Called by both the `IInputAdapter` push path\n * and the manual `track()` call path.\n *\n * Steps:\n * 1. Normalize state label\n * 2. Update state model (markSeen + recordTransition)\n * 3. Evaluate entropy signal → emit `high_entropy` if triggered\n * 4. Evaluate trajectory signal → emit `trajectory_anomaly` if triggered\n * 5. Emit `state_change`\n * 6. Persist model state\n */\n private _processState(raw: string): void {\n // ── Normalize ────────────────────────────────────────────────────────────\n let state = normalizeRouteState(raw);\n\n if (this.stateNormalizer) {\n try {\n const normalized = this.stateNormalizer(state);\n if (typeof normalized !== 'string') {\n this.onError?.({\n code: 'VALIDATION',\n message: `IntentEngine.track(): stateNormalizer must return a string, got ${typeof normalized}`,\n });\n return;\n }\n // Empty string is a deliberate \"skip this state\" signal.\n if (normalized === '') return;\n state = normalized;\n } catch (err) {\n this.onError?.({\n code: 'VALIDATION',\n message: `IntentEngine.track(): stateNormalizer threw: ${\n err instanceof Error ? err.message : String(err)\n }`,\n });\n return;\n }\n }\n\n if (state === '') {\n this.onError?.({\n code: 'VALIDATION',\n message: 'IntentEngine.track(): state label must not be an empty string',\n });\n return;\n }\n\n const from = this.previousState;\n\n // ── Update state model ────────────────────────────────────────────────────\n // markSeen / recordTransition are state mutations — if either throws we abort\n // this track() call entirely. previousState has NOT been advanced yet so the\n // engine state remains consistent.\n try {\n this.stateModel.markSeen(state);\n } catch (err) {\n this.onError?.({\n code: 'STATE_MODEL',\n message: `IntentEngine: stateModel.markSeen() threw: ${\n err instanceof Error ? err.message : String(err)\n }`,\n });\n return;\n }\n if (from !== null) {\n try {\n this.stateModel.recordTransition(from, state);\n } catch (err) {\n this.onError?.({\n code: 'STATE_MODEL',\n message: `IntentEngine: stateModel.recordTransition() threw: ${\n err instanceof Error ? err.message : String(err)\n }`,\n });\n return;\n }\n }\n\n // Advance internal position before evaluation so signals see the new state.\n this.previousState = state;\n this.recentTrajectory.push(state);\n if (this.recentTrajectory.length > TRAJECTORY_WINDOW) this.recentTrajectory.shift();\n\n // ── Signal evaluation (transition-dependent) ─────────────────────────────\n // Evaluation methods are read-only — if they throw we skip that signal and\n // continue so state_change and persistence still fire.\n if (from !== null) {\n // Entropy signal\n let entropyResult = { entropy: 0, normalizedEntropy: 0, isHigh: false };\n try {\n entropyResult = this.stateModel.evaluateEntropy(state);\n } catch (err) {\n this.onError?.({\n code: 'STATE_MODEL',\n message: `IntentEngine: stateModel.evaluateEntropy() threw: ${\n err instanceof Error ? err.message : String(err)\n }`,\n });\n }\n if (entropyResult.isHigh) {\n this.emitter.emit('high_entropy', {\n state,\n entropy: entropyResult.entropy,\n normalizedEntropy: entropyResult.normalizedEntropy,\n });\n }\n\n // Trajectory anomaly signal\n let trajectoryResult = null;\n try {\n trajectoryResult = this.stateModel.evaluateTrajectory(from, state, this.recentTrajectory);\n } catch (err) {\n this.onError?.({\n code: 'STATE_MODEL',\n message: `IntentEngine: stateModel.evaluateTrajectory() threw: ${\n err instanceof Error ? err.message : String(err)\n }`,\n });\n }\n if (trajectoryResult !== null && trajectoryResult.isAnomalous) {\n const sampleSize = trajectoryResult.sampleSize;\n this.emitter.emit('trajectory_anomaly', {\n stateFrom: from,\n stateTo: state,\n realLogLikelihood: trajectoryResult.logLikelihood,\n expectedBaselineLogLikelihood: trajectoryResult.baselineLogLikelihood,\n zScore: trajectoryResult.zScore,\n sampleSize,\n confidence: sampleSize < 10 ? 'low' : sampleSize < 30 ? 'medium' : 'high',\n });\n }\n }\n\n // ── Emit state_change (always) ────────────────────────────────────────────\n this.emitter.emit('state_change', { from, to: state });\n\n // ── Persist ───────────────────────────────────────────────────────────────\n this._persist();\n }\n\n /** Serialize and save model state via IPersistenceAdapter. */\n private _persist(): void {\n try {\n this.persistence.save(this.storageKey, this.stateModel.serialize());\n } catch (err) {\n this.onError?.({\n code: 'STORAGE_WRITE',\n message: `IntentEngine: persistence.save() threw: ${\n err instanceof Error ? err.message : String(err)\n }`,\n });\n }\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/**\n * BrowserLifecycleAdapter — web plugin for ILifecycleAdapter\n * --------------------------------------------------------\n * Bridges the browser's Page Visibility API and DOM interaction events\n * into the microkernel's ILifecycleAdapter contract.\n *\n * This plugin re-exports and explicitly types the existing\n * BrowserLifecycleAdapter implementation from the core adapters module,\n * making the interface contract visible at the plugin layer.\n *\n * The concrete class satisfies ILifecycleAdapter structurally (same method\n * signatures), and the subclass declaration below makes that relationship\n * explicit so tooling and consumers can rely on it without inspection.\n *\n * Events wired:\n * - `document.visibilitychange` → onPause / onResume callbacks\n * - `mousemove`, `scroll`, `touchstart`, `keydown` → onInteraction callbacks\n * (throttled to ≤ 1 per 1 000 ms)\n * - `document.documentElement` mouseleave (clientY ≤ 0) → onExitIntent callbacks\n */\n\nimport { BrowserLifecycleAdapter as _Base } from '../../adapters.js';\nimport type { ILifecycleAdapter } from '../../types/microkernel.js';\n\n/**\n * Standard browser lifecycle adapter for the IntentEngine microkernel.\n *\n * Drop-in for any environment where `document` and `window` are available.\n * All DOM access is guarded with `typeof document !== 'undefined'` checks so\n * the module can be imported in SSR / Node.js without throwing.\n *\n * ```ts\n * import { BrowserLifecycleAdapter } from '@passiveintent/core/plugins/web';\n *\n * const engine = new IntentEngine({\n * lifecycle: new BrowserLifecycleAdapter(),\n * // …\n * });\n * ```\n */\nexport class BrowserLifecycleAdapter extends _Base implements ILifecycleAdapter {}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/**\n * MouseKinematicsAdapter — web plugin for IInputAdapter\n * --------------------------------------------------------\n * Converts browser navigation and pointer/scroll physics into the engine's\n * canonical state string stream.\n *\n * Two classes of state are emitted:\n *\n * 1. **Navigation states** — emitted on `popstate` / `hashchange` events\n * and on first `subscribe()` call. State label = `location.pathname`.\n *\n * 2. **Scroll-depth sub-states** — emitted when the user crosses a scroll\n * depth threshold within the current page. State label format:\n * `{pathname}@scroll.{percent}` (e.g. `/product/details@scroll.50`).\n * Thresholds: 25 %, 50 %, 75 %, 100 %.\n *\n * 3. **Mouse-velocity states** — emitted when the pointer velocity crosses\n * the boundary between \"scanning\" (fast) and \"focused\" (slow/stopped).\n * State labels: `{pathname}@velocity.scanning`, `{pathname}@velocity.focused`.\n * This gives the intent engine a signal for reading-depth vs. rapid browsing.\n *\n * All DOM access is guarded so the module is safe to import in SSR/Node.js\n * environments (subscribe will be a no-op and return a no-op unsubscriber).\n *\n * Usage:\n * ```ts\n * const engine = new IntentEngine({\n * input: new MouseKinematicsAdapter(),\n * // …\n * });\n * ```\n */\n\nimport type { IInputAdapter } from '../../types/microkernel.js';\n\n/** Scroll depth thresholds at which sub-states are emitted (percent). */\nconst SCROLL_THRESHOLDS = [25, 50, 75, 100] as const;\n\n/** Pointer velocity (px/ms) below which the user is considered \"focused\". */\nconst FOCUSED_VELOCITY_THRESHOLD = 0.3;\n\n/** Minimum interval between mousemove velocity samples (ms). */\nconst VELOCITY_SAMPLE_INTERVAL_MS = 200;\n\n/** Debounce delay for scroll depth evaluation (ms). */\nconst SCROLL_DEBOUNCE_MS = 150;\n\nexport class MouseKinematicsAdapter implements IInputAdapter {\n private callback: ((state: string) => void) | null = null;\n\n /** Registered DOM event removers — collected in subscribe(), drained in destroy(). */\n private readonly cleanups: Array<() => void> = [];\n\n /* ── Scroll tracking ─────────────────────────────────────────── */\n /** Last scroll threshold percent that was emitted. */\n private lastScrollPercent: number = -1;\n private scrollDebounceTimer: ReturnType<typeof setTimeout> | null = null;\n\n /* ── Mouse velocity tracking ─────────────────────────────────── */\n private lastMouseX = 0;\n private lastMouseY = 0;\n private lastMouseTime = 0;\n /** Last velocity zone emitted: 'scanning' | 'focused' | null (not yet emitted). */\n private lastVelocityZone: 'scanning' | 'focused' | null = null;\n\n /* ── Current page path ───────────────────────────────────────── */\n private currentPath = '';\n\n /* ================================================================= */\n /* IInputAdapter */\n /* ================================================================= */\n\n subscribe(onState: (state: string) => void): () => void {\n if (typeof window === 'undefined') {\n // SSR / non-browser: return a no-op unsubscriber.\n return () => {};\n }\n\n this.callback = onState;\n this.currentPath = window.location.pathname;\n this.lastScrollPercent = -1;\n this.lastVelocityZone = null;\n\n // ── Emit initial page state ──────────────────────────────────────\n // Deferred via queueMicrotask so callers can register engine.on() listeners\n // before the first state_change fires. The engine constructor calls\n // subscribe() synchronously, meaning any .on() registrations that happen\n // after createBrowserIntent() returns would otherwise miss this event.\n queueMicrotask(() => onState(this.currentPath));\n\n // ── Navigation events ────────────────────────────────────────────\n // Covers back/forward (popstate) and hash-based routing (hashchange).\n //\n // NOTE — push-state SPAs (React Router, Next.js App Router, Vue Router, …)\n // use history.pushState / history.replaceState, which do NOT fire popstate.\n // Monkeypatching those methods is intentionally avoided here: it produces\n // global side-effects that compose poorly when multiple adapters or routers\n // are present on the same page.\n //\n // For push-state SPAs, use one of:\n // 1. A custom IInputAdapter that calls `engine.track()` inside the\n // router's navigation hook (e.g. React Router `history.listen`,\n // Vue Router `router.afterEach`, Next.js `router.events.on`).\n // 2. The raw IntentEngine path: `new IntentEngine({ input: myAdapter })`.\n const onPopState = (): void => this.handleNavigation();\n const onHashChange = (): void => this.handleNavigation();\n\n window.addEventListener('popstate', onPopState);\n window.addEventListener('hashchange', onHashChange);\n this.cleanups.push(() => {\n window.removeEventListener('popstate', onPopState);\n window.removeEventListener('hashchange', onHashChange);\n });\n\n // ── Scroll depth events ──────────────────────────────────────────\n const onScroll = (): void => this.scheduleScrollEvaluation();\n\n window.addEventListener('scroll', onScroll, { passive: true });\n this.cleanups.push(() => window.removeEventListener('scroll', onScroll));\n\n // ── Mouse velocity events ────────────────────────────────────────\n const onMouseMove = (e: MouseEvent): void => this.sampleMouseVelocity(e);\n\n window.addEventListener('mousemove', onMouseMove, { passive: true });\n this.cleanups.push(() => window.removeEventListener('mousemove', onMouseMove));\n\n return () => this.teardown();\n }\n\n destroy(): void {\n this.teardown();\n }\n\n /* ================================================================= */\n /* Navigation */\n /* ================================================================= */\n\n private handleNavigation(): void {\n if (typeof window === 'undefined') return;\n\n this.currentPath = window.location.pathname;\n\n // Cancel any pending scroll debounce from the previous page.\n if (this.scrollDebounceTimer !== null) {\n clearTimeout(this.scrollDebounceTimer);\n this.scrollDebounceTimer = null;\n }\n\n // Reset sub-state tracking for the new page.\n this.lastScrollPercent = -1;\n this.lastVelocityZone = null;\n\n // Reset mouse velocity tracking to prevent stale velocity calculations.\n this.lastMouseTime = 0;\n this.lastMouseX = 0;\n this.lastMouseY = 0;\n\n this.emit(this.currentPath);\n }\n\n /* ================================================================= */\n /* Scroll depth */\n /* ================================================================= */\n\n private scheduleScrollEvaluation(): void {\n if (this.scrollDebounceTimer !== null) {\n clearTimeout(this.scrollDebounceTimer);\n }\n this.scrollDebounceTimer = setTimeout(() => {\n this.scrollDebounceTimer = null;\n this.evaluateScrollDepth();\n }, SCROLL_DEBOUNCE_MS);\n }\n\n private evaluateScrollDepth(): void {\n if (typeof window === 'undefined' || typeof document === 'undefined') return;\n\n const scrollY = window.scrollY;\n const docHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight;\n\n if (docHeight <= 0) return;\n\n const percent = Math.min(100, Math.round((scrollY / docHeight) * 100));\n\n // Find the highest threshold crossed.\n let crossed: number | null = null;\n for (const threshold of SCROLL_THRESHOLDS) {\n if (percent >= threshold && threshold > this.lastScrollPercent) {\n crossed = threshold;\n }\n }\n\n if (crossed !== null) {\n this.lastScrollPercent = crossed;\n this.emit(`${this.currentPath}@scroll.${crossed}`);\n }\n }\n\n /* ================================================================= */\n /* Mouse velocity */\n /* ================================================================= */\n\n private sampleMouseVelocity(e: MouseEvent): void {\n const now = typeof performance !== 'undefined' ? performance.now() : Date.now();\n\n if (this.lastMouseTime === 0) {\n // First sample — seed without emitting.\n this.lastMouseX = e.clientX;\n this.lastMouseY = e.clientY;\n this.lastMouseTime = now;\n return;\n }\n\n const dt = now - this.lastMouseTime;\n if (dt < VELOCITY_SAMPLE_INTERVAL_MS) return;\n\n const dx = e.clientX - this.lastMouseX;\n const dy = e.clientY - this.lastMouseY;\n const distance = Math.sqrt(dx * dx + dy * dy);\n const velocity = distance / dt; // px/ms\n\n this.lastMouseX = e.clientX;\n this.lastMouseY = e.clientY;\n this.lastMouseTime = now;\n\n const zone: 'scanning' | 'focused' =\n velocity >= FOCUSED_VELOCITY_THRESHOLD ? 'scanning' : 'focused';\n\n if (zone !== this.lastVelocityZone) {\n this.lastVelocityZone = zone;\n this.emit(`${this.currentPath}@velocity.${zone}`);\n }\n }\n\n /* ================================================================= */\n /* Internal helpers */\n /* ================================================================= */\n\n private emit(state: string): void {\n this.callback?.(state);\n }\n\n private teardown(): void {\n for (const cleanup of this.cleanups) cleanup();\n this.cleanups.length = 0;\n\n if (this.scrollDebounceTimer !== null) {\n clearTimeout(this.scrollDebounceTimer);\n this.scrollDebounceTimer = null;\n }\n\n this.callback = null;\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/**\n * ContinuousGraphModel — web plugin for IStateModel\n * --------------------------------------------------------\n * Implements IStateModel for standard web routing by composing the existing\n * `MarkovGraph` (transition modeling + signal evaluation) and `BloomFilter`\n * (state membership) primitives.\n *\n * Evaluation logic mirrors `SignalEngine` exactly — same constants, same\n * z-score formula, same calibration path — so ContinuousGraphModel + IntentEngine\n * produce numerically identical signals to IntentManager when configured\n * identically.\n *\n * Serialization format (JSON string):\n * ```json\n * { \"bloomBase64\": \"<base64>\", \"graphBinary\": \"<base64>\" }\n * ```\n * Matches the wire format used by `PersistenceCoordinator` / `SyncPersistStrategy`\n * so payloads are compatible with existing persisted data.\n *\n * Configuration:\n * ```ts\n * const model = new ContinuousGraphModel({\n * graph: { highEntropyThreshold: 0.75, divergenceThreshold: 3.5 },\n * bloom: { bitSize: 2048, hashCount: 4 },\n * baseline: exportedGraph, // from IntentManager.exportGraph()\n * });\n * ```\n */\n\nimport { MarkovGraph } from '../../core/markov.js';\nimport { BloomFilter } from '../../core/bloom.js';\nimport { uint8ToBase64, base64ToUint8 } from '../../persistence/codec.js';\nimport type { IStateModel, EntropyResult, TrajectoryResult } from '../../types/microkernel.js';\nimport type { MarkovGraphConfig, BloomFilterConfig } from '../../types/events.js';\nimport type { SerializedMarkovGraph } from '../../core/markov.js';\nimport {\n MIN_SAMPLE_TRANSITIONS,\n MIN_WINDOW_LENGTH,\n MAX_WINDOW_LENGTH,\n SMOOTHING_EPSILON,\n} from '../../engine/constants.js';\n\nexport interface ContinuousGraphModelConfig {\n /**\n * Markov graph tuning: entropy threshold, divergence threshold, smoothing, etc.\n * Defaults match IntentManager's defaults.\n */\n graph?: MarkovGraphConfig;\n /** Bloom filter sizing. Default: bitSize=2048, hashCount=4. */\n bloom?: BloomFilterConfig;\n /**\n * Optional pre-trained baseline graph (from `IntentManager.exportGraph()` or\n * `MarkovGraph.toJSON()`). Required for `trajectory_anomaly` detection — when\n * absent, `evaluateTrajectory()` always returns `null`.\n */\n baseline?: SerializedMarkovGraph;\n}\n\n/** Wire format stored by `serialize()` / parsed by `restore()`. */\ninterface PersistedPayload {\n bloomBase64: string;\n graphBinary: string;\n}\n\nexport class ContinuousGraphModel implements IStateModel {\n private graph: MarkovGraph;\n private bloom: BloomFilter;\n private readonly baseline: MarkovGraph | null;\n private readonly graphConfig: MarkovGraphConfig;\n private readonly bloomConfig: BloomFilterConfig;\n\n constructor(config: ContinuousGraphModelConfig = {}) {\n this.graphConfig = config.graph ?? {};\n this.bloomConfig = config.bloom ?? {};\n this.graph = new MarkovGraph(this.graphConfig);\n this.bloom = new BloomFilter(this.bloomConfig);\n this.baseline = config.baseline\n ? MarkovGraph.fromJSON(config.baseline, this.graphConfig)\n : null;\n }\n\n /* ================================================================= */\n /* IStateModel — membership */\n /* ================================================================= */\n\n markSeen(state: string): void {\n this.bloom.add(state);\n }\n\n hasSeen(state: string): boolean {\n return this.bloom.check(state);\n }\n\n /* ================================================================= */\n /* IStateModel — transitions */\n /* ================================================================= */\n\n recordTransition(from: string, to: string): void {\n this.graph.incrementTransition(from, to);\n }\n\n getLikelyNext(state: string, threshold: number): { state: string; probability: number }[] {\n return this.graph.getLikelyNextStates(state, threshold);\n }\n\n /* ================================================================= */\n /* IStateModel — signal evaluation */\n /* ================================================================= */\n\n /**\n * Evaluate whether the current state's outgoing entropy is anomalously high.\n *\n * Guards:\n * - Fewer than `MIN_SAMPLE_TRANSITIONS` outgoing edges → not enough data.\n *\n * Mirrors `SignalEngine.evaluateEntropy()`.\n */\n evaluateEntropy(state: string): EntropyResult {\n const NOT_HIGH: EntropyResult = { entropy: 0, normalizedEntropy: 0, isHigh: false };\n\n if (this.graph.rowTotal(state) < MIN_SAMPLE_TRANSITIONS) {\n return NOT_HIGH;\n }\n\n const entropy = this.graph.entropyForState(state);\n const normalizedEntropy = this.graph.normalizedEntropyForState(state);\n const isHigh = normalizedEntropy >= this.graph.highEntropyThreshold;\n\n return { entropy, normalizedEntropy, isHigh };\n }\n\n /**\n * Evaluate whether the `from → to` transition is anomalous relative to the\n * baseline distribution.\n *\n * Guards:\n * - No baseline configured → `null`.\n * - Trajectory shorter than `MIN_WINDOW_LENGTH` → `null` (warm-up phase).\n *\n * When `baselineMeanLL` + `baselineStdLL` are configured on the graph, uses\n * z-score comparison. Otherwise falls back to raw average LL threshold.\n *\n * Mirrors `SignalEngine.evaluateTrajectory()`.\n */\n evaluateTrajectory(\n from: string,\n _to: string,\n trajectory: readonly string[],\n ): TrajectoryResult | null {\n if (this.baseline === null) return null;\n if (trajectory.length < MIN_WINDOW_LENGTH) return null;\n\n const real = MarkovGraph.logLikelihoodTrajectory(this.graph, trajectory, SMOOTHING_EPSILON);\n const expected = MarkovGraph.logLikelihoodTrajectory(\n this.baseline,\n trajectory,\n SMOOTHING_EPSILON,\n );\n\n const N = Math.max(1, trajectory.length - 1);\n const expectedAvg = expected / N;\n const threshold = -Math.abs(this.graph.divergenceThreshold);\n\n const hasCalibratedBaseline =\n typeof this.graph.baselineMeanLL === 'number' &&\n typeof this.graph.baselineStdLL === 'number' &&\n Number.isFinite(this.graph.baselineMeanLL) &&\n Number.isFinite(this.graph.baselineStdLL) &&\n this.graph.baselineStdLL > 0;\n\n const adjustedStd = hasCalibratedBaseline\n ? this.graph.baselineStdLL! * Math.sqrt(MAX_WINDOW_LENGTH / N)\n : 0;\n\n const zScore = hasCalibratedBaseline\n ? (expectedAvg - this.graph.baselineMeanLL!) / adjustedStd\n : expectedAvg;\n\n const isAnomalous = hasCalibratedBaseline ? zScore <= threshold : expectedAvg <= threshold;\n\n return {\n zScore,\n isAnomalous,\n logLikelihood: real,\n baselineLogLikelihood: expected,\n sampleSize: this.graph.rowTotal(from),\n };\n }\n\n /* ================================================================= */\n /* IStateModel — serialization */\n /* ================================================================= */\n\n /**\n * Serialize the live Markov graph and Bloom filter to a JSON string.\n *\n * Wire format matches `SyncPersistStrategy` so payloads written by\n * `IntentManager` / `PersistenceCoordinator` can be loaded here, and\n * vice versa.\n */\n serialize(): string {\n this.graph.prune();\n const graphBinary = uint8ToBase64(this.graph.toBinary());\n const bloomBase64 = this.bloom.toBase64();\n const payload: PersistedPayload = { bloomBase64, graphBinary };\n return JSON.stringify(payload);\n }\n\n /**\n * Restore the Markov graph and Bloom filter from a previously serialized string.\n * Throws on parse failure so `IntentEngine` can surface it through `onError`.\n */\n restore(serialized: string): void {\n const payload = JSON.parse(serialized) as Partial<PersistedPayload>;\n\n if (payload.graphBinary) {\n this.graph = MarkovGraph.fromBinary(base64ToUint8(payload.graphBinary), this.graphConfig);\n }\n\n if (payload.bloomBase64) {\n this.bloom = BloomFilter.fromBase64(payload.bloomBase64, this.bloomConfig);\n }\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/**\n * LocalStorageAdapter — web plugin for IPersistenceAdapter\n * --------------------------------------------------------\n * Implements IPersistenceAdapter on top of the browser's `window.localStorage`.\n *\n * Every access is guarded with `typeof window === 'undefined'` checks so the\n * module can be imported in SSR / Node.js / Web Worker environments without\n * throwing. When `window.localStorage` is unavailable the adapter silently\n * degrades to a no-op (load returns `null`, save is skipped).\n *\n * Note: `localStorage` is synchronous. For async backends (React Native\n * AsyncStorage, Capacitor Preferences, IndexedDB) use `IntentManager.createAsync()`\n * with an `AsyncStorageAdapter` — that path remains in Layer 3 (IntentManager).\n *\n * Usage:\n * ```ts\n * import { LocalStorageAdapter } from '@passiveintent/core/plugins/web';\n *\n * const engine = new IntentEngine({\n * persistence: new LocalStorageAdapter(),\n * // …\n * });\n * ```\n */\n\nimport type { IPersistenceAdapter } from '../../types/microkernel.js';\n\nexport class LocalStorageAdapter implements IPersistenceAdapter {\n /**\n * Load a value from localStorage.\n * Returns `null` when the key is absent, when localStorage is unavailable\n * (SSR, incognito with storage blocked, sandboxed iframe), or when a\n * `SecurityError` is thrown.\n */\n load(key: string): string | null {\n try {\n if (typeof window === 'undefined' || !window.localStorage) return null;\n return window.localStorage.getItem(key);\n } catch {\n // SecurityError accessing window.localStorage on sandboxed/opaque origins,\n // or SecurityError / other errors from getItem.\n return null;\n }\n }\n\n /**\n * Save a value to localStorage.\n * Silently no-ops when localStorage is unavailable.\n *\n * Unlike `BrowserStorageAdapter.setItem`, this method also catches and\n * **swallows** `QuotaExceededError` so that a full storage partition does\n * not surface an uncaught exception. Higher-layer error handling\n * (IntentEngine's `onError` callback) is the right place to observe this\n * failure; the caller (`IntentEngine._persist()`) wraps this call in its\n * own try/catch and routes any thrown error through `onError`.\n */\n save(key: string, value: string): void {\n try {\n if (typeof window === 'undefined' || !window.localStorage) return;\n window.localStorage.setItem(key, value);\n } catch {\n // SecurityError (sandboxed/opaque origin) or QuotaExceededError — swallowed.\n }\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/**\n * Progressive Disclosure layer — createBrowserIntent\n * --------------------------------------------------------\n * The 90 % use-case entry point for standard web applications.\n *\n * This factory wires all standard web plugins (BrowserLifecycleAdapter,\n * MouseKinematicsAdapter, ContinuousGraphModel, LocalStorageAdapter) into a\n * raw IntentEngine and returns the fully operational instance.\n *\n * Callers see ONE config object with no adapter boilerplate. The dependency-\n * injection complexity that powers the microkernel's plug-and-play flexibility\n * is invisible here — it exists for the 10 % who need React Native, Electron,\n * or custom fintech/food-delivery adapters via `new IntentEngine({…})` directly.\n *\n * ```ts\n * // Standard web — one line:\n * const intent = createBrowserIntent({ storageKey: 'my-app' });\n *\n * intent.on('high_entropy', ({ state }) => showHelpWidget(state));\n * intent.on('exit_intent', ({ likelyNext }) => prefetch(likelyNext));\n *\n * // Manual tracking alongside automatic kinematics:\n * intent.track('/checkout/review');\n *\n * // SPA teardown:\n * intent.destroy();\n * ```\n *\n * Architecture layers\n * -------------------\n * Layer 1 — Core algorithms : MarkovGraph, BloomFilter (pure, no I/O)\n * Layer 2 — Microkernel : IntentEngine (adapter interfaces)\n * Layer 3 — Web factory ← YOU ARE HERE (progressive disclosure)\n * Layer 4 — Framework SDKs : usePassiveIntent (React hook) (wraps IntentManager)\n */\n\nimport { IntentEngine } from './engine/intent-engine.js';\nimport { BrowserLifecycleAdapter } from './plugins/web/BrowserLifecycleAdapter.js';\nimport { MouseKinematicsAdapter } from './plugins/web/MouseKinematicsAdapter.js';\nimport { ContinuousGraphModel } from './plugins/web/ContinuousGraphModel.js';\nimport { LocalStorageAdapter } from './plugins/web/LocalStorageAdapter.js';\nimport type { MarkovGraphConfig, BloomFilterConfig } from './types/events.js';\nimport type { SerializedMarkovGraph } from './core/markov.js';\n\n/* ------------------------------------------------------------------ */\n/* BrowserConfig */\n/* ------------------------------------------------------------------ */\n\n/**\n * User-facing configuration for `createBrowserIntent`.\n *\n * All fields are optional — `createBrowserIntent({})` is a valid, fully-\n * operational call that uses sensible defaults for every setting.\n */\nexport interface BrowserConfig {\n /**\n * localStorage key used to persist the Bloom filter and Markov graph\n * across sessions.\n *\n * Use a unique key per application to avoid collisions when multiple\n * PassiveIntent instances share the same origin.\n *\n * Default: `'passive-intent-engine'`\n */\n storageKey?: string;\n\n /**\n * Pre-trained baseline graph exported from a previous session via\n * `IntentManager.exportGraph()` or `MarkovGraph.toJSON()`.\n *\n * Required for `trajectory_anomaly` detection. Without a baseline,\n * the engine emits `high_entropy` and `exit_intent` events but silently\n * skips trajectory z-score comparisons.\n */\n baseline?: SerializedMarkovGraph;\n\n /**\n * Markov graph tuning parameters. Omit to accept production-tested defaults:\n * - `highEntropyThreshold`: 0.75 (normalized entropy above which `high_entropy` fires)\n * - `divergenceThreshold`: 3.5 (z-score magnitude for `trajectory_anomaly`)\n * - `smoothingAlpha`: 0.1 (Dirichlet regularization — prevents cold-start spikes)\n * - `maxStates`: 500 (LFU eviction cap)\n */\n graph?: MarkovGraphConfig;\n\n /**\n * Bloom filter sizing. Omit to accept defaults (`bitSize: 2048, hashCount: 4`).\n * Use `BloomFilter.computeOptimal(expectedRoutes, targetFPR)` to size precisely.\n */\n bloom?: BloomFilterConfig;\n\n /**\n * Optional custom state normalizer applied **after** the built-in\n * `normalizeRouteState()` (which strips query strings, hashes, trailing\n * slashes, UUIDs, and numeric IDs ≥ 4 digits).\n *\n * Use this to collapse dynamic slugs the built-in normalizer misses:\n * ```ts\n * stateNormalizer: (s) => s.replace(/^\\/blog\\/[^/]+$/, '/blog/:slug')\n * ```\n * Returning an empty string silently drops that `track()` call.\n */\n stateNormalizer?: (state: string) => string;\n\n /**\n * Non-fatal error callback. The engine never throws — storage errors,\n * quota exhaustion, parse failures, and invalid API calls are all routed\n * here so the host app can log or alert without a try/catch at every call\n * site.\n *\n * ```ts\n * onError: ({ code, message }) => Sentry.captureMessage(message, { tags: { code } })\n * ```\n */\n onError?: (error: { code: string; message: string }) => void;\n}\n\n/* ------------------------------------------------------------------ */\n/* Factory */\n/* ------------------------------------------------------------------ */\n\n/**\n * Create a fully wired `IntentEngine` for standard browser environments.\n *\n * Automatically instantiates and injects:\n * - `ContinuousGraphModel` — Markov graph + Bloom filter state model\n * - `LocalStorageAdapter` — `window.localStorage` persistence\n * - `BrowserLifecycleAdapter` — Page Visibility API lifecycle events\n * - `MouseKinematicsAdapter` — URL navigation + scroll-depth + mouse-velocity input\n *\n * @param config Optional tuning parameters. Every field has a sensible default.\n * @returns A live `IntentEngine` instance, ready to receive `on()` subscriptions\n * and `track()` calls. Call `destroy()` in SPA teardown paths.\n *\n * @example Minimal setup\n * ```ts\n * const intent = createBrowserIntent();\n * intent.on('high_entropy', ({ state, normalizedEntropy }) => {\n * console.log('Confused navigation detected:', state, normalizedEntropy);\n * });\n * ```\n *\n * @example With baseline for trajectory anomaly detection\n * ```ts\n * const intent = createBrowserIntent({\n * storageKey: 'acme-shop',\n * baseline: importedBaselineGraph,\n * onError: ({ code, message }) => logger.warn(code, message),\n * });\n * ```\n */\nexport function createBrowserIntent(config: BrowserConfig = {}): IntentEngine {\n const stateModel = new ContinuousGraphModel({\n graph: config.graph,\n bloom: config.bloom,\n baseline: config.baseline,\n });\n\n const persistence = new LocalStorageAdapter();\n const lifecycle = new BrowserLifecycleAdapter();\n const input = new MouseKinematicsAdapter();\n\n return new IntentEngine({\n stateModel,\n persistence,\n lifecycle,\n input,\n storageKey: config.storageKey,\n stateNormalizer: config.stateNormalizer,\n onError: config.onError,\n });\n}\n"],"mappings":"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","names":["index_exports","__export","ATTENTION_RETURN_THRESHOLD_MS","AnomalyDispatcher","BenchmarkRecorder","BloomFilter","BroadcastSync","BrowserLifecycleAdapter","BrowserStorageAdapter","BrowserTimerAdapter","DriftProtectionPolicy","EventEmitter","IDLE_CHECK_INTERVAL_MS","IntentEngine","IntentManager","MAX_PLAUSIBLE_DWELL_MS","MAX_STATE_LENGTH","MarkovGraph","MemoryStorageAdapter","PropensityCalculator","SignalEngine","USER_IDLE_THRESHOLD_MS","computeBloomConfig","createBrowserIntent","normalizeRouteState","__toCommonJS","uint8ToBase64","bytes","parts","i","slice","base64ToUint8","base64","binary","arr","fnv1a","input","seed","hash","i","_mc","v","_scratchHashes","BloomFilter","_BloomFilter","config","existingBits","byteSize","item","h1","h2","index","expectedItems","targetFPR","ln2","ln2Sq","m","k","insertedItemsCount","exponent","bitZeroProbability","uint8ToBase64","base64","base64ToUint8","bitIndex","byteIndex","mask","computeBloomConfig","falsePositiveRate","estimatedFpRate","MarkovGraph","_MarkovGraph","config","rawSmoothingAlpha","state","existing","index","fromState","toState","from","to","row","nextCount","count","entropy","p","k","denominator","numUnobserved","pUnobserved","supportSize","maxEntropy","normalized","baseline","sequence","epsilon","sum","i","minProbability","fromIndex","results","toIndex","probability","label","b","total","liveCount","ranked","idx","a","evictTarget","evictSet","removedTotal","deadIdx","rows","edges","data","graph","tombstoneSet","r","e","encoder","totalSize","encodedLabels","buffer","view","offset","encoded","decoder","version","numStates","rawLabels","strLen","numFreed","numRows","numEdges","toCounts","createAccumulator","recordSample","acc","elapsedMs","maxSamples","percentile","sorted","p","idx","toOperationStats","a","b","textEncoder","BenchmarkRecorder","config","operation","startedAt","memoryFootprint","payload","asText","BrowserStorageAdapter","key","value","BrowserTimerAdapter","fn","delay","id","MemoryStorageAdapter","_BrowserLifecycleAdapter","cb","callback","idx","now","opts","evt","root","e","BrowserLifecycleAdapter","UUID_V4_RE","MONGO_ID_RE","NUMERIC_ID_RE","normalizeRouteState","url","qIdx","path","hIdx","MAX_PLAUSIBLE_DWELL_MS","ATTENTION_RETURN_THRESHOLD_MS","USER_IDLE_THRESHOLD_MS","IDLE_CHECK_INTERVAL_MS","fprToZScore","fpr","p","t","buildIntentManagerOptions","config","botProtection","dwellTimeEnabled","crossTabSync","rawHoldoutPct","holdoutPercent","graphFPR","graphConfig","configuredSmoothing","trajectorySmoothingEpsilon","storageKey","rawPersistDebounce","persistDebounceMs","rawPersistThrottle","persistThrottleMs","rawEventCooldown","eventCooldownMs","rawDwellMinSamples","dwellTimeMinSamples","dwellFPR","rawDwellZScore","dwellTimeZScoreThreshold","enableBigrams","rawBigramThreshold","bigramFrequencyThreshold","rawDriftMaxRate","driftMaxAnomalyRate","rawDriftWindowMs","driftEvaluationWindowMs","rawHesitationMs","hesitationCorrelationWindowMs","EventEmitter","event","listener","set","payload","EntropyGuard","timestamp","previous","count","windowBotScore","deltas","oldestIndex","i","currIdx","nextIdx","delta","mean","variance","d","updateDwellStats","current","dwellMs","previousCount","previousMean","previousM2","count","delta","meanMs","delta2","m2","dwellStd","stats","assertNever","x","AnomalyDispatcher","config","decision","now","getConfidence","sampleSize","SignalEngine","config","EntropyGuard","AnomalyDispatcher","now","_from","_to","_trajectory","decision","state","start","entropy","normalizedEntropy","from","to","trajectory","real","MarkovGraph","expected","N","expectedAvg","threshold","hasCalibratedBaseline","adjustedStd","zScore","shouldEmit","dwellMs","updated","updateDwellStats","std","dwellStd","BasePersistStrategy","ctx","data","graph","bloom","graphBinary","graphBytes","uint8ToBase64","err","throttleMs","elapsed","remainingMs","SyncPersistStrategy","payload","isQuota","AsyncPersistStrategy","asyncStorage","setItemPromise","PersistenceCoordinator","config","AsyncPersistStrategy","SyncPersistStrategy","health","code","message","err","graph","bloom","graphConfig","raw","parsed","bytes","base64ToUint8","MarkovGraph","BloomFilter","payloadByteLength","LifecycleCoordinator","config","BrowserLifecycleAdapter","adapter","hiddenDuration","currentState","owns","armIdleTimer","unsub","idleMs","DwellTimePolicy","config","ctx","dwellMs","BigramPolicy","graph","bigramFrequencyThreshold","from","to","trajectory","bigramFrom","bigramTo","DriftProtectionPolicy","maxAnomalyRate","evaluationWindowMs","now","MAX_STATE_LENGTH","isValidTransitionMessage","msg","isValidCounterMessage","BroadcastSync","channelName","graph","bloom","counters","event","from","to","key","by","current","data","CrossTabSyncPolicy","config","BroadcastSync","from","to","key","by","IntentManager","_IntentManager","config","EventEmitter","ctx","bloomAddStart","i","incrementStart","opts","buildIntentManagerOptions","BenchmarkRecorder","BrowserTimerAdapter","persistenceCoordinator","PersistenceCoordinator","BrowserStorageAdapter","restored","BloomFilter","MarkovGraph","driftPolicy","DriftProtectionPolicy","policies","SignalEngine","DwellTimePolicy","state","dwellMs","BigramPolicy","CrossTabSyncPolicy","LifecycleCoordinator","delta","candidates","event","listener","storageKey","raw","preloadBridge","_omit","restConfig","normalizeRouteState","normalized","coerced","err","now","trackStart","start","seen","threshold","sanitize","payload","key","by","next","serialized","PropensityCalculator","alpha","throttleMs","graph","currentState","targetState","maxDepth","safeMaxDepth","queue","accumulated","node","neighbours","nextState","edgeProb","reachProb","nextPathVisited","currentZScore","now","safeZ","frictionPenalty","propensity","TRAJECTORY_WINDOW","IntentEngine","config","EventEmitter","raw","err","unsubInput","state","candidates","event","listener","teardown","normalizeRouteState","normalized","from","entropyResult","trajectoryResult","sampleSize","BrowserLifecycleAdapter","SCROLL_THRESHOLDS","FOCUSED_VELOCITY_THRESHOLD","VELOCITY_SAMPLE_INTERVAL_MS","SCROLL_DEBOUNCE_MS","MouseKinematicsAdapter","onState","onPopState","onHashChange","onScroll","onMouseMove","e","scrollY","docHeight","percent","crossed","threshold","now","dt","dx","dy","velocity","zone","state","cleanup","ContinuousGraphModel","config","MarkovGraph","BloomFilter","state","from","to","threshold","NOT_HIGH","entropy","normalizedEntropy","isHigh","_to","trajectory","real","expected","N","expectedAvg","hasCalibratedBaseline","adjustedStd","zScore","isAnomalous","graphBinary","uint8ToBase64","payload","serialized","base64ToUint8","LocalStorageAdapter","key","value","createBrowserIntent","config","stateModel","ContinuousGraphModel","persistence","LocalStorageAdapter","lifecycle","BrowserLifecycleAdapter","input","MouseKinematicsAdapter","IntentEngine"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/persistence/codec.ts","../src/core/bloom.ts","../src/core/markov.ts","../src/performance-instrumentation.ts","../src/adapters.ts","../src/utils/route-normalizer.ts","../src/engine/constants.ts","../src/engine/config-normalizer.ts","../src/engine/event-emitter.ts","../src/engine/entropy-guard.ts","../src/engine/dwell.ts","../src/engine/anomaly-dispatcher.ts","../src/engine/signal-engine.ts","../src/engine/persistence-strategies.ts","../src/engine/persistence-coordinator.ts","../src/engine/lifecycle-coordinator.ts","../src/engine/policies/dwell-time-policy.ts","../src/engine/policies/bigram-policy.ts","../src/engine/policies/drift-protection-policy.ts","../src/sync/broadcast-sync.ts","../src/engine/policies/cross-tab-sync-policy.ts","../src/engine/intent-manager.ts","../src/engine/propensity-calculator.ts","../src/engine/intent-engine.ts","../src/plugins/web/BrowserLifecycleAdapter.ts","../src/plugins/web/MouseKinematicsAdapter.ts","../src/plugins/web/ContinuousGraphModel.ts","../src/plugins/web/LocalStorageAdapter.ts","../src/factory.ts"],"sourcesContent":["/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/**\n * PassiveIntent — Public API Barrel Export\n * --------------------------------------------------------\n * Import everything you need from one clean entry-point:\n *\n * import { IntentManager, BloomFilter, MarkovGraph } from '@passiveintent/core';\n */\n\n/* ---- Core SDK ---- */\nexport {\n BloomFilter,\n computeBloomConfig,\n MarkovGraph,\n IntentManager,\n BroadcastSync,\n MAX_STATE_LENGTH,\n MAX_PLAUSIBLE_DWELL_MS,\n ATTENTION_RETURN_THRESHOLD_MS,\n USER_IDLE_THRESHOLD_MS,\n IDLE_CHECK_INTERVAL_MS,\n normalizeRouteState,\n AnomalyDispatcher,\n SignalEngine,\n EventEmitter,\n DriftProtectionPolicy,\n BenchmarkRecorder,\n} from './intent-sdk.js';\n\nexport type {\n IntentEventName,\n IntentEventMap,\n HighEntropyPayload,\n TrajectoryAnomalyPayload,\n DwellTimeAnomalyPayload,\n StateChangePayload,\n BotDetectedPayload,\n HesitationDetectedPayload,\n SessionStalePayload,\n AttentionReturnPayload,\n UserIdlePayload,\n UserResumedPayload,\n ExitIntentPayload,\n ConversionPayload,\n PassiveIntentTelemetry,\n BloomFilterConfig,\n MarkovGraphConfig,\n IntentManagerConfig,\n PassiveIntentError,\n DwellTimeConfig,\n SerializedMarkovGraph,\n AnomalyDecision,\n EntropyDecision,\n TrajectoryDecision,\n DwellDecision,\n AnomalyDispatcherConfig,\n AnomalyEventEmitter,\n DriftProtectionPolicyLike,\n SignalEngineConfig,\n} from './intent-sdk.js';\n\n/* ---- Adapters ---- */\nexport {\n BrowserStorageAdapter,\n BrowserTimerAdapter,\n MemoryStorageAdapter,\n BrowserLifecycleAdapter,\n} from './adapters.js';\n\nexport type {\n StorageAdapter,\n AsyncStorageAdapter,\n TimerAdapter,\n TimerHandle,\n LifecycleAdapter,\n} from './adapters.js';\n\n/* ---- Performance Instrumentation ---- */\nexport type {\n BenchmarkConfig,\n MemoryFootprintReport,\n OperationStats,\n PerformanceReport,\n} from './performance-instrumentation.js';\n\n/* ================================================================== */\n/* Microkernel — Layer 2 */\n/* ================================================================== */\n\n/**\n * Raw IntentEngine class for enterprise / cross-platform use cases.\n *\n * Requires explicit injection of all four adapter interfaces.\n * Use `createBrowserIntent()` instead for standard web applications.\n *\n * ```ts\n * import { IntentEngine } from '@passiveintent/core';\n *\n * const engine = new IntentEngine({\n * stateModel: myModel,\n * persistence: myStorage,\n * lifecycle: myLifecycle,\n * input: myInput,\n * });\n * ```\n */\nexport { PropensityCalculator } from './engine/propensity-calculator.js';\nexport { IntentEngine } from './engine/intent-engine.js';\nexport type { IntentEngineConfig } from './types/microkernel.js';\n\n/* ================================================================== */\n/* Web Factory — Layer 3 (Progressive Disclosure) */\n/* ================================================================== */\n\n/**\n * `createBrowserIntent` — primary entry point for standard web applications.\n *\n * Automatically wires `ContinuousGraphModel`, `LocalStorageAdapter`,\n * `BrowserLifecycleAdapter`, and `MouseKinematicsAdapter` into a new\n * `IntentEngine` and returns it ready to use.\n *\n * ```ts\n * import { createBrowserIntent } from '@passiveintent/core';\n *\n * const intent = createBrowserIntent({ storageKey: 'my-app' });\n * intent.on('high_entropy', ({ state }) => showHelpWidget(state));\n * intent.on('exit_intent', ({ likelyNext }) => prefetch(likelyNext));\n * ```\n */\nexport { createBrowserIntent } from './factory.js';\nexport type { BrowserConfig } from './factory.js';\n\n/* ================================================================== */\n/* CoreInterfaces namespace — enterprise plugin contracts */\n/* ================================================================== */\n\n/**\n * TypeScript contracts for building custom plugins against the IntentEngine\n * microkernel. Import the namespace to implement your own adapters:\n *\n * ```ts\n * import type { CoreInterfaces } from '@passiveintent/core';\n *\n * // React Native navigation adapter\n * class ReactNativeInputAdapter implements CoreInterfaces.IInputAdapter {\n * subscribe(onState: (state: string) => void): () => void {\n * return navigation.addListener('state', (e) => onState(e.data.state.routes.at(-1)?.name ?? '/'));\n * }\n * destroy(): void {}\n * }\n *\n * // Custom swipe-based input for dating / food-delivery apps\n * class SwipeKinematicsAdapter implements CoreInterfaces.IInputAdapter {\n * subscribe(onState: (state: string) => void): () => void {\n * return swipeEmitter.on('swipe', ({ direction, cardId }) =>\n * onState(`card:${cardId}:${direction}`));\n * }\n * destroy(): void {}\n * }\n * ```\n *\n * Available contracts:\n * - `IInputAdapter` — push-based navigation/behavioral input\n * - `ILifecycleAdapter` — platform pause / resume / exit-intent\n * - `IStateModel` — Markov + Bloom state model\n * - `IPersistenceAdapter` — synchronous key-value storage\n * - `IntentEngineConfig` — full constructor config shape\n * - `EntropyResult` — return type of `IStateModel.evaluateEntropy`\n * - `TrajectoryResult` — return type of `IStateModel.evaluateTrajectory`\n */\nexport type * as CoreInterfaces from './types/microkernel.js';\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/**\n * Lightweight base64 codec for Uint8Array payloads.\n *\n * Browser `btoa()` only accepts binary strings, not typed arrays directly.\n * The standard pattern `btoa(String.fromCharCode(...bytes))` uses spread +\n * Function.prototype.apply which throws a RangeError (\"Maximum call stack\n * size exceeded\") for arrays larger than ~65k bytes – a realistic size for\n * a large MarkovGraph binary payload. This module uses a chunked approach\n * to avoid that limit while remaining dependency-free.\n */\n\n/**\n * Convert a Uint8Array to a base64 string using chunked\n * String.fromCharCode to avoid O(n) string concatenation.\n */\nexport function uint8ToBase64(bytes: Uint8Array): string {\n /**\n * 0x8000 (32 768) bytes per chunk is safely below the JavaScript engine’s\n * maximum function argument count (~65k arguments in V8 / SpiderMonkey),\n * ensuring `String.fromCharCode.apply` never overflows the call stack.\n */\n const CHUNK = 0x8000;\n const parts: string[] = [];\n for (let i = 0; i < bytes.length; i += CHUNK) {\n const slice = bytes.subarray(i, Math.min(i + CHUNK, bytes.length));\n parts.push(String.fromCharCode.apply(null, slice as unknown as number[]));\n }\n return btoa(parts.join(''));\n}\n\n/** Convert base64 payload back to Uint8Array. */\nexport function base64ToUint8(base64: string): Uint8Array {\n const binary = atob(base64);\n const arr = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i += 1) {\n arr[i] = binary.charCodeAt(i);\n }\n return arr;\n}\n\n/**\n * Versioned codec for localStorage persistence.\n *\n * A 1-byte version header is prepended to every serialized payload so that\n * future format changes can be detected on restore rather than silently\n * producing corrupt state. The IntentManager's `onError` boundary catches\n * errors with `code === 'RESTORE_PARSE'` and falls back to a cold start.\n */\nexport const CURRENT_CODEC_VERSION = 0x01;\n\n/** Serialize bytes to a versioned base64 string. */\nexport function serialize(bytes: Uint8Array): string {\n const versioned = new Uint8Array(1 + bytes.length);\n versioned[0] = CURRENT_CODEC_VERSION;\n versioned.set(bytes, 1);\n return uint8ToBase64(versioned);\n}\n\n/** Deserialize a versioned base64 string back to raw bytes. */\nexport function deserialize(base64: string): Uint8Array {\n const versioned = base64ToUint8(base64);\n if (versioned.length === 0) {\n throw Object.assign(new Error('Codec version mismatch: empty or invalid payload'), {\n code: 'RESTORE_PARSE',\n });\n }\n if (versioned[0] !== CURRENT_CODEC_VERSION) {\n throw Object.assign(\n new Error(\n `Codec version mismatch: expected 0x${CURRENT_CODEC_VERSION.toString(16).padStart(2, '0')}, ` +\n `got 0x${versioned[0].toString(16).padStart(2, '0')}`,\n ),\n { code: 'RESTORE_PARSE' },\n );\n }\n return versioned.subarray(1);\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport { base64ToUint8, uint8ToBase64 } from '../persistence/codec.js';\nimport type { BloomFilterConfig } from '../types/events.js';\n\n/**\n * FNV-1a hash with a configurable seed.\n *\n * Used in a Kirsch-Mitzenmacher double-hashing scheme:\n * h_i(x) = (h1(x) + i * h2(x)) mod m\n *\n * Two independent hashes (h1 via seed 0x811c9dc5, h2 via seed 0x01000193)\n * are combined to derive `hashCount` virtual hash functions with a single\n * pair of underlying hash calls. This avoids computing k distinct hash\n * functions while preserving near-optimal false-positive rates.\n * Reference: Kirsch & Mitzenmacher (2006), \"Less Hashing, Same Performance\".\n */\nfunction fnv1a(input: string, seed = 0x811c9dc5): number {\n let hash = seed >>> 0;\n for (let i = 0; i < input.length; i += 1) {\n hash ^= input.charCodeAt(i);\n hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);\n }\n return hash >>> 0;\n}\n\n// Mix-constant resolved once at module load; runtime indirection prevents the\n// bundler from proving the value statically and eliding it via constant folding.\nconst _mc = ((): number => {\n const v = (globalThis as { __PFIE_WM__?: unknown }).__PFIE_WM__;\n return (typeof v === 'number' ? v : 0x8badf00d) >>> 0;\n})();\n\n/** Shared scratch buffer for the two FNV-1a outputs used during hash derivation. */\nconst _scratchHashes = new Uint32Array(2);\n\n/**\n * Space-efficient probabilistic set membership test.\n *\n * Guarantees no false negatives (anything added will always be found).\n * Allows tunable false positives — use `computeOptimal` to size the filter\n * for a target FPR given an expected item count.\n *\n * The bit array can be serialized to base64 via `toBase64()` and restored\n * via `fromBase64()`, enabling persistence across sessions without storing\n * the raw state strings (privacy-preserving by design).\n */\nexport class BloomFilter {\n readonly bitSize: number;\n readonly hashCount: number;\n private readonly bits: Uint8Array;\n\n /**\n * @param config Optional sizing parameters (bitSize, hashCount).\n * @param existingBits Pre-populated bit array from a prior `toBase64()` round-trip.\n * Must match the expected `byteSize` derived from `bitSize` or\n * it will be silently discarded and a fresh array allocated.\n */\n constructor(config: BloomFilterConfig = {}, existingBits?: Uint8Array) {\n this.bitSize = config.bitSize ?? 2048;\n this.hashCount = config.hashCount ?? 4;\n\n const byteSize = Math.ceil(this.bitSize / 8);\n this.bits =\n existingBits && existingBits.length === byteSize ? existingBits : new Uint8Array(byteSize);\n }\n\n add(item: string): void {\n this.computeHashes(item);\n const h1 = _scratchHashes[0];\n const h2 = _scratchHashes[1];\n for (let i = 0; i < this.hashCount; i += 1) {\n const index = ((h1 + i * h2) >>> 0) % this.bitSize;\n this.setBit(index);\n }\n }\n\n check(item: string): boolean {\n this.computeHashes(item);\n const h1 = _scratchHashes[0];\n const h2 = _scratchHashes[1];\n for (let i = 0; i < this.hashCount; i += 1) {\n const index = ((h1 + i * h2) >>> 0) % this.bitSize;\n if (!this.getBit(index)) return false;\n }\n return true;\n }\n\n /**\n * Compute the optimal filter size for a given load and false-positive rate.\n *\n * Formulas (standard Bloom filter theory):\n * m = ceil( -n * ln(p) / ln(2)^2 ) — optimal bit count\n * k = max(1, round( (m/n) * ln(2) )) — optimal hash function count\n *\n * where n = expectedItems, p = targetFPR.\n */\n static computeOptimal(\n expectedItems: number,\n targetFPR: number,\n ): { bitSize: number; hashCount: number } {\n if (expectedItems <= 0) return { bitSize: 8, hashCount: 1 };\n if (targetFPR <= 0) targetFPR = 1e-10;\n if (targetFPR >= 1) targetFPR = 0.99;\n\n const ln2 = Math.LN2;\n const ln2Sq = ln2 * ln2;\n\n const m = Math.ceil(-(expectedItems * Math.log(targetFPR)) / ln2Sq);\n const k = Math.max(1, Math.round((m / expectedItems) * ln2));\n\n return { bitSize: m, hashCount: k };\n }\n\n estimateCurrentFPR(insertedItemsCount: number): number {\n if (insertedItemsCount <= 0) return 0;\n const exponent = -(this.hashCount * insertedItemsCount) / this.bitSize;\n const bitZeroProbability = Math.exp(exponent);\n return Math.pow(1 - bitZeroProbability, this.hashCount);\n }\n\n getBitsetByteSize(): number {\n return this.bits.byteLength;\n }\n\n toBase64(): string {\n return uint8ToBase64(this.bits);\n }\n\n static fromBase64(base64: string, config: BloomFilterConfig = {}): BloomFilter {\n return new BloomFilter(config, base64ToUint8(base64));\n }\n\n private setBit(bitIndex: number): void {\n const byteIndex = bitIndex >> 3;\n const mask = 1 << (bitIndex & 7);\n this.bits[byteIndex] |= mask;\n }\n\n private getBit(bitIndex: number): boolean {\n const byteIndex = bitIndex >> 3;\n const mask = 1 << (bitIndex & 7);\n return (this.bits[byteIndex] & mask) !== 0;\n }\n\n private computeHashes(item: string): void {\n _scratchHashes[0] = (fnv1a(item, 0x811c9dc5) ^ _mc ^ _mc) >>> 0;\n _scratchHashes[1] = (fnv1a(item, 0x01000193) ^ _mc ^ _mc) >>> 0;\n }\n}\n\n/**\n * Compute the optimal Bloom filter bit size and hash function count for a\n * given workload, and return the estimated false-positive rate that results\n * from those rounded parameters.\n *\n * Use this as a tree-shakeable, class-free alternative to\n * `BloomFilter.computeOptimal` when you only need the sizing math without\n * importing the full filter implementation.\n *\n * @param expectedItems The number of unique routes or states the\n * application is expected to track (e.g. 200 if your\n * SPA has ~200 distinct URL patterns). If this value\n * is less than or equal to 0, it is clamped to 1.\n * @param falsePositiveRate Target false-positive probability expressed as a\n * float ideally in the range (0, 1). For example,\n * pass `0.01` for a 1 % false-positive rate. Values\n * less than or equal to 0 are clamped to `1e-10`, and\n * values greater than or equal to 1 are clamped to\n * `0.99`.\n * @returns `{ bitSize, hashCount, estimatedFpRate }` where `estimatedFpRate`\n * is the actual FPR achieved after rounding `m` and `k` to integers.\n *\n * @example\n * const { bitSize, hashCount, estimatedFpRate } = computeBloomConfig(200, 0.01);\n * // → { bitSize: 1918, hashCount: 7, estimatedFpRate: ~0.009 }\n * const intent = new IntentManager({ bloom: { bitSize, hashCount } });\n */\nexport function computeBloomConfig(\n expectedItems: number,\n falsePositiveRate: number,\n): { bitSize: number; hashCount: number; estimatedFpRate: number } {\n if (expectedItems <= 0) expectedItems = 1;\n if (falsePositiveRate <= 0) falsePositiveRate = 1e-10;\n if (falsePositiveRate >= 1) falsePositiveRate = 0.99;\n\n // Standard optimal Bloom filter formulas:\n // m = ceil( -(n * ln(p)) / ln(2)^2 )\n // k = round( (m / n) * ln(2) )\n const m = Math.ceil(-(expectedItems * Math.log(falsePositiveRate)) / (Math.LN2 * Math.LN2));\n const k = Math.max(1, Math.round((m / expectedItems) * Math.log(2)));\n\n // Recalculate the actual FPR achieved with the rounded (integer) m and k.\n // Formula: p_actual = (1 - e^(-k*n/m))^k\n const bitZeroProbability = Math.exp(-(k * expectedItems) / m);\n const estimatedFpRate = Math.pow(1 - bitZeroProbability, k);\n\n return { bitSize: m, hashCount: k, estimatedFpRate };\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport type { MarkovGraphConfig } from '../types/events.js';\n\n/**\n * Outgoing transition counts for a single source state.\n *\n * Using a nested `Map<number, number>` keeps the representation sparse:\n * states that never transition to each other consume no memory. For typical\n * navigation graphs (5–50 states, fan-out 2–8) this is cheaper than a dense\n * N×N matrix and avoids serializing zero entries in the binary codec.\n */\ninterface TransitionRow {\n /** Sum of all outgoing transition counts from this state. Pre-maintained to avoid O(k) map iteration on every probability query. */\n total: number;\n /** Destination state index → raw observation count. */\n toCounts: Map<number, number>;\n}\n\nexport interface SerializedMarkovGraph {\n /** index → state label; '' marks a tombstone (freed) slot */\n states: string[];\n /** sparse rows: [fromIndex, total, [toIndex, count][]] */\n rows: Array<[number, number, Array<[number, number]>]>;\n /** explicit list of freed (tombstoned) slot indices */\n freedIndices: number[];\n}\n\n/**\n * Sparse Markov graph for first-order (and optional second-order bigram) state transitions.\n *\n * **Index stability design:**\n * States are assigned an integer index on first encounter and that index\n * is never re-numbered. This makes the binary codec trivially verifiable\n * (indices in the encoded rows map directly to the states array) and means\n * serialized data stays valid even after LFU pruning, because pruned slots\n * are *tombstoned* (set to `''`) rather than compacted.\n *\n * **Sparse Map representation:**\n * `rows` is a `Map<fromIndex, TransitionRow>` rather than a flat array.\n * Navigation graphs are typically sparse (most state pairs are never\n * observed), so flat N×N matrices would waste memory and inflate encoded size.\n */\nexport class MarkovGraph {\n private readonly rows = new Map<number, TransitionRow>();\n private readonly stateToIndex = new Map<string, number>();\n private readonly indexToState: string[] = [];\n private readonly freedIndices: number[] = [];\n\n readonly highEntropyThreshold: number;\n readonly divergenceThreshold: number;\n readonly smoothingEpsilon: number;\n readonly baselineMeanLL?: number;\n readonly baselineStdLL?: number;\n readonly maxStates: number;\n /**\n * Dirichlet / Laplace smoothing pseudo-count.\n * When > 0, `getProbability` uses:\n * P = (count + alpha) / (total + alpha * k)\n * where k = number of live states. When 0, falls back to exact\n * frequentist math (count / total) with zero extra cost.\n *\n * **Default: `0.1`** — mild Bayesian regularization that prevents\n * cold-start 100 % probability spikes on Day-1 sessions.\n * Pass `0` explicitly to restore pure frequentist behaviour.\n *\n * Non-finite or negative values are clamped to `0` by the constructor.\n */\n readonly smoothingAlpha: number;\n\n constructor(config: MarkovGraphConfig = {}) {\n this.highEntropyThreshold = config.highEntropyThreshold ?? 0.75;\n this.divergenceThreshold = Math.abs(config.divergenceThreshold ?? 3.5);\n this.smoothingEpsilon = config.smoothingEpsilon ?? 0.01;\n this.baselineMeanLL = config.baselineMeanLL;\n this.baselineStdLL = config.baselineStdLL;\n this.maxStates = config.maxStates ?? 500;\n const rawSmoothingAlpha = config.smoothingAlpha ?? 0.1;\n this.smoothingAlpha =\n Number.isFinite(rawSmoothingAlpha) && rawSmoothingAlpha >= 0 ? rawSmoothingAlpha : 0;\n }\n\n /**\n * Return the integer index for a state label, allocating a new slot if\n * needed. Reuses tombstoned (freed) slots from LFU pruning before\n * appending to the end of `indexToState`, keeping the array compact.\n *\n * Empty string is rejected because `''` is the internal tombstone marker.\n *\n * ⚠ **Multi-call safety:** `ensureState` is NOT safe to call twice in sequence\n * without first ensuring no prune can fire between the two calls. If a caller\n * resolves index A via `ensureState(a)`, then a prune fires before `ensureState(b)`,\n * state A may be tombstoned (it was just allocated so its `total = 0`, making it the\n * first LFU eviction candidate). Any subsequent write using index A then lands on\n * a dead slot — a \"ghost row\". Any method that calls `ensureState` more than once\n * MUST execute the prune guard BEFORE the first call, never between calls.\n * See `incrementTransition` for the canonical pattern.\n */\n ensureState(state: string): number {\n if (state === '') throw new Error('MarkovGraph: state label must not be empty string');\n const existing = this.stateToIndex.get(state);\n if (existing !== undefined) return existing;\n\n let index: number;\n if (this.freedIndices.length > 0) {\n index = this.freedIndices.pop()!;\n this.indexToState[index] = state;\n } else {\n index = this.indexToState.length;\n this.indexToState.push(state);\n }\n this.stateToIndex.set(state, index);\n return index;\n }\n\n incrementTransition(fromState: string, toState: string): void {\n // Burst-bloat guard: prune BEFORE resolving any indices so that neither\n // fromState nor toState can be tombstoned mid-resolution. Placing this\n // inside ensureState caused a ghost-row bug: a freshly-allocated fromState\n // index (total=0) would be evicted by the prune triggered during the\n // subsequent ensureState(toState) call, leaving a phantom row at a\n // tombstoned index with no stateToIndex entry.\n if (this.stateToIndex.size >= this.maxStates * 1.5) {\n this.prune();\n }\n const from = this.ensureState(fromState);\n const to = this.ensureState(toState);\n\n const row = this.rows.get(from) ?? { total: 0, toCounts: new Map<number, number>() };\n const nextCount = (row.toCounts.get(to) ?? 0) + 1;\n row.toCounts.set(to, nextCount);\n row.total += 1;\n this.rows.set(from, row);\n }\n\n /**\n * P(to|from) from live counts, with optional Bayesian Laplace smoothing.\n *\n * When `smoothingAlpha > 0`:\n * P = (count + α) / (total + α × k)\n * where k = live-state count (`stateToIndex.size`).\n * α = 0 falls back to exact frequentist math with no overhead.\n *\n * No allocations are made during this call.\n */\n getProbability(fromState: string, toState: string): number {\n const from = this.stateToIndex.get(fromState);\n const to = this.stateToIndex.get(toState);\n if (from === undefined || to === undefined) return 0;\n\n const row = this.rows.get(from);\n if (!row || row.total === 0) return 0;\n\n const count = row.toCounts.get(to) ?? 0;\n if (this.smoothingAlpha === 0) {\n return count / row.total;\n }\n return (\n (count + this.smoothingAlpha) / (row.total + this.smoothingAlpha * this.stateToIndex.size)\n );\n }\n\n /**\n * Entropy H(i) = -Σ P(i->j) log P(i->j)\n * Returned entropy is in nats (natural log).\n *\n * When `smoothingAlpha > 0`, smoothed probabilities are used for ALL k\n * states (observed + unobserved). The contribution from the\n * `(k - observed)` unobserved transitions is computed analytically —\n * no temporary arrays are allocated.\n */\n entropyForState(state: string): number {\n const from = this.stateToIndex.get(state);\n if (from === undefined) return 0;\n\n const row = this.rows.get(from);\n if (!row || row.total === 0) return 0;\n\n let entropy = 0;\n if (this.smoothingAlpha === 0) {\n row.toCounts.forEach((count) => {\n const p = count / row.total;\n if (p > 0) entropy -= p * Math.log(p);\n });\n } else {\n const k = this.stateToIndex.size;\n const denominator = row.total + this.smoothingAlpha * k;\n\n // Observed transitions\n row.toCounts.forEach((count) => {\n const p = (count + this.smoothingAlpha) / denominator;\n entropy -= p * Math.log(p);\n });\n\n // Unobserved transitions — computed analytically to avoid allocation\n const numUnobserved = k - row.toCounts.size;\n if (numUnobserved > 0) {\n const pUnobserved = this.smoothingAlpha / denominator;\n if (pUnobserved > 0) {\n entropy -= numUnobserved * pUnobserved * Math.log(pUnobserved);\n }\n }\n }\n return entropy;\n }\n\n /**\n * Normalized entropy in [0..1], dividing by max entropy ln(k)\n * where k is the **local** branching factor (number of observed outgoing\n * edges from this state, floored at 2 to avoid division artifacts).\n *\n * Using the local fan-out instead of the global state count ensures that\n * the normalized score correctly reflects *local* decision-space confusion\n * (rage-clicking between 3–4 links) regardless of how many total pages\n * exist in the graph. With the global denominator, ln(N) grows without\n * bound as users browse more pages, crushing the normalized score and\n * making high-entropy anomalies undetectable after ~10 unique states.\n *\n * **Smoothing note:** this method uses raw frequentist counts\n * (count / row.total) for both the entropy numerator and the ln(supportSize)\n * denominator so that both are anchored to the same local support.\n * `entropyForState()` uses Bayesian Laplace smoothing over all k global\n * states and is intentionally a different (larger) quantity — calling it\n * here would make the numerator exceed the denominator whenever the graph\n * has many states, producing raw normalized scores > 1 that the clamp would\n * silently mask.\n */\n normalizedEntropyForState(state: string): number {\n const from = this.stateToIndex.get(state);\n if (from === undefined) return 0;\n\n const row = this.rows.get(from);\n if (!row || row.total === 0) return 0;\n\n const supportSize = Math.max(2, row.toCounts.size);\n const maxEntropy = Math.log(supportSize);\n if (maxEntropy <= 0) return 0;\n\n // Compute entropy using only the observed local transitions (frequentist).\n // This keeps the numerator and denominator anchored to the same support\n // (local fan-out), avoiding the mismatch that would arise from calling\n // entropyForState() which spreads probability mass over all k global states\n // in Bayesian mode.\n let entropy = 0;\n row.toCounts.forEach((count) => {\n const p = count / row.total;\n if (p > 0) entropy -= p * Math.log(p);\n });\n\n // ── Invariant: entropy ≤ maxEntropy (should hold WITHOUT the clamp) ────────\n // By the maximum-entropy principle, H(p) = -Σ p_i ln(p_i) ≤ ln(k) for any\n // probability distribution over k outcomes. Here k = supportSize and\n // maxEntropy = ln(supportSize), so entropy / maxEntropy ∈ [0, 1] by construction.\n //\n // Math.min(1, ...) below is a SAFETY NET only — it must never be the\n // mechanism that produces a value ≤ 1. If it fires, the numerator and\n // denominator are using different distributions (regression indicator).\n // Property-based tests in unit-fast.test.mjs assert this invariant directly.\n const normalized = entropy / maxEntropy;\n return Math.min(1, Math.max(0, normalized));\n }\n\n /**\n * Log-likelihood trajectory:\n * log L = Σ log P_baseline(s_t+1 | s_t)\n *\n * To avoid -Infinity when a transition doesn't exist in baseline,\n * apply epsilon smoothing.\n */\n static logLikelihoodTrajectory(\n baseline: MarkovGraph,\n sequence: readonly string[],\n epsilon = 0.01,\n ): number {\n if (sequence.length < 2) return 0;\n\n let sum = 0;\n for (let i = 0; i < sequence.length - 1; i += 1) {\n const p = baseline.getProbability(sequence[i], sequence[i + 1]);\n sum += Math.log(p > 0 ? p : epsilon);\n }\n return sum;\n }\n\n /**\n * Returns all outgoing edges from `fromState` whose transition probability\n * meets or exceeds `minProbability`, sorted descending by probability.\n *\n * Intended for **read-only** UI prefetching hints. The returned state\n * labels are raw values from the internal transition graph and may include\n * sensitive routes.\n *\n * ⚠ **Security notice — you MUST filter results before acting on them.**\n * Always pass a `sanitize` predicate (see `IntentManager.predictNextStates`)\n * that rejects state-mutating or privacy-sensitive routes such as\n * `/logout`, `/checkout/pay`, or any route containing PII.\n * Prefetching must **never** trigger state-mutating side effects.\n *\n * @param fromState The source state to query outgoing transitions from.\n * @param minProbability Minimum probability threshold in [0, 1] (inclusive).\n * Values ≤ 0 return all edges; values > 1 return none.\n * @returns Array of `{ state, probability }` objects, sorted by probability\n * descending. Returns an empty array when the state is unknown or\n * has no recorded transitions.\n */\n getLikelyNextStates(\n fromState: string,\n minProbability: number,\n ): { state: string; probability: number }[] {\n const fromIndex = this.stateToIndex.get(fromState);\n if (fromIndex === undefined) return [];\n\n const row = this.rows.get(fromIndex);\n if (!row || row.total === 0) return [];\n\n const results: { state: string; probability: number }[] = [];\n // Pre-compute smoothing denominator once — O(1), no allocation.\n const denominator =\n this.smoothingAlpha === 0\n ? row.total\n : row.total + this.smoothingAlpha * this.stateToIndex.size;\n\n row.toCounts.forEach((count, toIndex) => {\n const probability =\n this.smoothingAlpha === 0\n ? count / denominator\n : (count + this.smoothingAlpha) / denominator;\n if (probability >= minProbability) {\n const label = this.indexToState[toIndex];\n if (label && label !== '') {\n results.push({ state: label, probability });\n }\n }\n });\n\n results.sort((a, b) => b.probability - a.probability);\n return results;\n }\n\n /**\n * Returns the total number of outgoing transitions recorded for a state.\n * Used as a minimum-sample guard before firing entropy/divergence events.\n */\n rowTotal(state: string): number {\n const from = this.stateToIndex.get(state);\n if (from === undefined) return 0;\n return this.rows.get(from)?.total ?? 0;\n }\n\n /**\n * Total number of allocated index slots, including tombstoned (freed) ones.\n *\n * ⚠ This is NOT the count of live states. Use `stateToIndex.size`\n * (via `prune` or `fromJSON`) when you need the live count.\n * The value is useful for sizing serialization buffers.\n */\n stateCount(): number {\n return this.indexToState.length;\n }\n\n totalTransitions(): number {\n let total = 0;\n this.rows.forEach((row) => {\n total += row.total;\n });\n return total;\n }\n\n /**\n * LFU (Least-Frequently-Used) pruning.\n *\n * When the number of live states exceeds `maxStates`, evict the bottom\n * ~20 % of states ranked by total outgoing transitions. Rather than\n * re-indexing (which would invalidate every edge reference), pruned\n * states are \"tombstoned\":\n *\n * 1. Their outgoing row is deleted from `this.rows`.\n * 2. Any inbound edges referencing them are removed from other rows.\n * 3. The slot in `indexToState` is set to '' (dead index) and the\n * entry is removed from `stateToIndex`.\n *\n * This is O(S + E) where S = stateCount and E = total edge entries.\n */\n prune(): void {\n const liveCount = this.stateToIndex.size;\n if (liveCount <= this.maxStates) return;\n\n // ── 1. Rank every live state by total outgoing transitions (LFU) ──\n // Map.forEach callback: (value: number, key: string) where value = index.\n const ranked: Array<{ index: number; total: number }> = [];\n this.stateToIndex.forEach((idx) => {\n const row = this.rows.get(idx);\n ranked.push({ index: idx, total: row?.total ?? 0 });\n });\n\n // Sort ascending by total transitions (lowest first = least used).\n ranked.sort((a, b) => a.total - b.total);\n\n // Evict bottom 20 % (at least 1, at most enough to get back to maxStates).\n const evictTarget = Math.max(\n 1,\n Math.min(Math.ceil(liveCount * 0.2), liveCount - this.maxStates),\n );\n const evictSet = new Set<number>();\n for (let i = 0; i < evictTarget && i < ranked.length; i += 1) {\n evictSet.add(ranked[i].index);\n }\n\n // ── 2. Remove outgoing rows for evicted states ──\n evictSet.forEach((idx) => {\n this.rows.delete(idx);\n });\n\n // ── 3. Scrub inbound edges from surviving rows ──\n this.rows.forEach((row) => {\n let removedTotal = 0;\n evictSet.forEach((deadIdx) => {\n const count = row.toCounts.get(deadIdx);\n if (count !== undefined) {\n removedTotal += count;\n row.toCounts.delete(deadIdx);\n }\n });\n row.total -= removedTotal;\n });\n\n // ── 4. Tombstone index slots and register for reuse ──\n evictSet.forEach((idx) => {\n const label = this.indexToState[idx];\n if (label !== undefined && label !== '') {\n this.stateToIndex.delete(label);\n }\n this.freedIndices.push(idx);\n this.indexToState[idx] = ''; // dead slot\n });\n }\n\n toJSON(): SerializedMarkovGraph {\n const rows: SerializedMarkovGraph['rows'] = [];\n this.rows.forEach((row, fromIndex) => {\n const edges: Array<[number, number]> = [];\n row.toCounts.forEach((count, toIndex) => {\n edges.push([toIndex, count]);\n });\n rows.push([fromIndex, row.total, edges]);\n });\n\n return {\n states: [...this.indexToState],\n rows,\n freedIndices: [...this.freedIndices],\n };\n }\n\n static fromJSON(data: SerializedMarkovGraph, config: MarkovGraphConfig = {}): MarkovGraph {\n const graph = new MarkovGraph(config);\n const tombstoneSet = new Set(data.freedIndices);\n\n for (let i = 0; i < data.states.length; i += 1) {\n const label = data.states[i];\n graph.indexToState.push(label);\n\n if (tombstoneSet.has(i)) {\n if (label !== '') {\n throw new Error(\n `MarkovGraph.fromJSON: slot ${i} is listed in freedIndices but has non-empty label \"${label}\"`,\n );\n }\n graph.freedIndices.push(i);\n } else {\n if (label === '') {\n throw new Error(\n `MarkovGraph.fromJSON: slot ${i} has an empty-string label but is not listed in ` +\n `freedIndices. Empty string is reserved as the tombstone marker.`,\n );\n }\n graph.stateToIndex.set(label, i);\n }\n }\n\n for (let r = 0; r < data.rows.length; r += 1) {\n const [fromIndex, total, edges] = data.rows[r];\n const row: TransitionRow = { total, toCounts: new Map<number, number>() };\n for (let e = 0; e < edges.length; e += 1) {\n const [toIndex, count] = edges[e];\n row.toCounts.set(toIndex, count);\n }\n graph.rows.set(fromIndex, row);\n }\n\n return graph;\n }\n\n /* ================================================================== */\n /* Binary serialization — zero-dependency, zero JSON.stringify */\n /* ================================================================== */\n\n /**\n * Binary wire format — version 0x02, little-endian throughout:\n *\n * ┌──────────────────────────────────────┐\n * │ Version : Uint8 (1B) │ — always 0x02\n * │ NumStates : Uint16 (2B) │\n * │ ┌── for each state ───────────────┐ │\n * │ │ StringByteLen : Uint16 (2B) │ │ — 0 bytes for tombstone slots\n * │ │ UTF-8 Bytes : [N] │ │\n * │ └─────────────────────────────────┘ │\n * │ NumFreedIndices : Uint16 (2B) │ — 0 if no tombstones\n * │ ┌── for each freed index ─────────┐ │\n * │ │ SlotIndex : Uint16 (2B) │ │\n * │ └─────────────────────────────────┘ │\n * │ NumRows : Uint16 (2B) │\n * │ ┌── for each row ──────────────────┐ │\n * │ │ FromIndex : Uint16 (2B) │ │\n * │ │ Total : Uint32 (4B) │ │\n * │ │ NumEdges : Uint16 (2B) │ │\n * │ │ ┌── for each edge ─────────────┐ │ │\n * │ │ │ ToIndex : Uint16 (2B) │ │ │\n * │ │ │ Count : Uint32 (4B) │ │ │\n * │ │ └──────────────────────────────┘ │ │\n * │ └──────────────────────────────────┘ │\n * └──────────────────────────────────────┘\n */\n toBinary(): Uint8Array {\n const encoder = new TextEncoder();\n\n // ── Pre-compute total buffer size so we allocate exactly once ──\n\n // Header: version (1B) + numStates (2B) = 3 bytes\n let totalSize = 3;\n\n // Encode all state labels to UTF-8 up front and cache the buffers.\n const encodedLabels: Uint8Array[] = new Array(this.indexToState.length);\n for (let i = 0; i < this.indexToState.length; i += 1) {\n encodedLabels[i] = encoder.encode(this.indexToState[i]);\n // Per state: stringByteLen (Uint16 = 2B) + actual bytes\n totalSize += 2 + encodedLabels[i].byteLength;\n }\n\n // V2 freed-index section: numFreedIndices (2B) + freed index values (2B each)\n totalSize += 2 + this.freedIndices.length * 2;\n\n // NumRows header: 2 bytes\n totalSize += 2;\n\n // Per row: fromIndex (2B) + total (4B) + numEdges (2B) = 8 bytes\n // Per edge: toIndex (2B) + count (4B) = 6 bytes\n this.rows.forEach((row) => {\n totalSize += 8; // row header\n totalSize += row.toCounts.size * 6; // edges\n });\n\n // ── Allocate buffer + DataView ──\n const buffer = new Uint8Array(totalSize);\n const view = new DataView(buffer.buffer);\n let offset = 0;\n\n // ── Write header ──\n\n // Byte 0: format version (0x02 — adds explicit freed-index list)\n view.setUint8(offset, 0x02); // version = 2\n offset += 1; // offset now 1\n\n // Bytes 1-2: number of states (Uint16 LE)\n view.setUint16(offset, this.indexToState.length, true);\n offset += 2; // offset now 3\n\n // ── Write state labels ──\n for (let i = 0; i < this.indexToState.length; i += 1) {\n const encoded = encodedLabels[i];\n\n // 2 bytes: UTF-8 byte length of this label\n view.setUint16(offset, encoded.byteLength, true);\n offset += 2;\n\n // N bytes: raw UTF-8 payload\n buffer.set(encoded, offset);\n offset += encoded.byteLength;\n }\n\n // ── Write freed-index list (V2) ──\n\n // 2 bytes: number of freed indices (Uint16 LE)\n view.setUint16(offset, this.freedIndices.length, true);\n offset += 2;\n\n // 2 bytes each: freed slot index (Uint16 LE)\n for (let i = 0; i < this.freedIndices.length; i += 1) {\n view.setUint16(offset, this.freedIndices[i], true);\n offset += 2;\n }\n\n // ── Write rows header ──\n\n // 2 bytes: number of rows with data\n view.setUint16(offset, this.rows.size, true);\n offset += 2;\n\n // ── Write each row ──\n this.rows.forEach((row, fromIndex) => {\n // 2 bytes: fromIndex (Uint16 LE)\n view.setUint16(offset, fromIndex, true);\n offset += 2;\n\n // 4 bytes: total outgoing transitions (Uint32 LE)\n view.setUint32(offset, row.total, true);\n offset += 4;\n\n // 2 bytes: number of edges (Uint16 LE)\n view.setUint16(offset, row.toCounts.size, true);\n offset += 2;\n\n // Per edge: toIndex (2B) + count (4B)\n row.toCounts.forEach((count, toIndex) => {\n // 2 bytes: destination state index (Uint16 LE)\n view.setUint16(offset, toIndex, true);\n offset += 2;\n\n // 4 bytes: transition count (Uint32 LE)\n view.setUint32(offset, count, true);\n offset += 4;\n });\n });\n\n return buffer;\n }\n\n /**\n * Reconstruct a MarkovGraph from the binary format produced by `toBinary()`.\n * See the encoding spec in `toBinary()` for the wire layout.\n */\n static fromBinary(buffer: Uint8Array, config: MarkovGraphConfig = {}): MarkovGraph {\n const graph = new MarkovGraph(config);\n const decoder = new TextDecoder();\n const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);\n let offset = 0;\n\n // ── Read header ──\n\n // Byte 0: version — only 0x02 is supported.\n const version = view.getUint8(offset);\n offset += 1;\n if (version !== 0x02) {\n throw new Error(\n `Unsupported MarkovGraph binary version: 0x${version.toString(16).padStart(2, '0')}. ` +\n `Only version 0x02 is supported.`,\n );\n }\n\n // Bytes 1-2: number of states (Uint16 LE)\n const numStates = view.getUint16(offset, true);\n offset += 2;\n\n // ── Read state labels into a temporary buffer ──\n // Classification (live vs tombstone) happens after reading the freed-index\n // list, so we store raw labels first and populate the graph below.\n const rawLabels: string[] = [];\n for (let i = 0; i < numStates; i += 1) {\n const strLen = view.getUint16(offset, true);\n offset += 2;\n rawLabels.push(decoder.decode(buffer.subarray(offset, offset + strLen)));\n offset += strLen;\n }\n\n // ── Read freed-index list and classify slots ──\n const numFreed = view.getUint16(offset, true);\n offset += 2;\n\n const tombstoneSet = new Set<number>();\n for (let i = 0; i < numFreed; i += 1) {\n tombstoneSet.add(view.getUint16(offset, true));\n offset += 2;\n }\n\n for (let i = 0; i < rawLabels.length; i += 1) {\n const label = rawLabels[i];\n graph.indexToState.push(label);\n if (tombstoneSet.has(i)) {\n if (label !== '') {\n throw new Error(\n `MarkovGraph.fromBinary: slot ${i} is listed as freed but has non-empty label \"${label}\"`,\n );\n }\n graph.freedIndices.push(i);\n } else {\n if (label === '') {\n throw new Error(\n `MarkovGraph.fromBinary: slot ${i} has an empty-string label but is not listed ` +\n `in the freed-index section. Empty string is reserved as the tombstone marker.`,\n );\n }\n graph.stateToIndex.set(label, i);\n }\n }\n\n // ── Read rows ──\n\n // 2 bytes: number of rows\n const numRows = view.getUint16(offset, true);\n offset += 2;\n\n for (let r = 0; r < numRows; r += 1) {\n // 2 bytes: fromIndex\n const fromIndex = view.getUint16(offset, true);\n offset += 2;\n\n // 4 bytes: total transitions\n const total = view.getUint32(offset, true);\n offset += 4;\n\n // 2 bytes: number of edges\n const numEdges = view.getUint16(offset, true);\n offset += 2;\n\n const toCounts = new Map<number, number>();\n for (let e = 0; e < numEdges; e += 1) {\n // 2 bytes: toIndex\n const toIndex = view.getUint16(offset, true);\n offset += 2;\n\n // 4 bytes: count\n const count = view.getUint32(offset, true);\n offset += 4;\n\n toCounts.set(toIndex, count);\n }\n\n graph.rows.set(fromIndex, { total, toCounts });\n }\n\n return graph;\n }\n}\n\n/**\n * Smoothing epsilon for log-likelihood calculations.\n * Must be identical between calibration and runtime.\n */\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nexport interface BenchmarkConfig {\n enabled?: boolean;\n maxSamples?: number;\n}\n\nexport interface OperationStats {\n count: number;\n avgMs: number;\n p95Ms: number;\n p99Ms: number;\n maxMs: number;\n}\n\nexport interface MemoryFootprintReport {\n stateCount: number;\n totalTransitions: number;\n bloomBitsetBytes: number;\n serializedGraphBytes: number;\n}\n\nexport interface PerformanceReport {\n benchmarkEnabled: boolean;\n track: OperationStats;\n bloomAdd: OperationStats;\n bloomCheck: OperationStats;\n incrementTransition: OperationStats;\n entropyComputation: OperationStats;\n divergenceComputation: OperationStats;\n memoryFootprint: MemoryFootprintReport;\n}\n\n/**\n * Circular-buffer reservoir for per-operation timing samples.\n *\n * Once `maxSamples` is reached, new samples overwrite old ones at position\n * `count % maxSamples` (not true reservoir / random sampling). This means\n * percentile estimates (p95, p99) are computed over the *most-recent*\n * `maxSamples` observations rather than a statistically unbiased sample of\n * all observations. For the SDK’s benchmark use-case (detecting regressions\n * in the steady-state hot path) recency bias is acceptable. If you need\n * unbiased percentiles across an arbitrary run duration, increase `maxSamples`\n * in the `BenchmarkConfig` to cover the full session.\n */\ninterface BenchmarkAccumulator {\n count: number;\n totalMs: number;\n maxMs: number;\n samples: number[];\n}\n\nconst DEFAULT_BENCHMARK_MAX_SAMPLES = 4096;\n\nfunction createAccumulator(): BenchmarkAccumulator {\n return { count: 0, totalMs: 0, maxMs: 0, samples: [] };\n}\n\nfunction recordSample(acc: BenchmarkAccumulator, elapsedMs: number, maxSamples: number): void {\n acc.count += 1;\n acc.totalMs += elapsedMs;\n if (elapsedMs > acc.maxMs) acc.maxMs = elapsedMs;\n if (acc.samples.length < maxSamples) {\n acc.samples.push(elapsedMs);\n } else if (maxSamples > 0) {\n acc.samples[acc.count % maxSamples] = elapsedMs;\n }\n}\n\nfunction percentile(sorted: number[], p: number): number {\n if (sorted.length === 0) return 0;\n const idx = Math.max(0, Math.min(sorted.length - 1, Math.ceil(p * sorted.length) - 1));\n return sorted[idx];\n}\n\nfunction toOperationStats(acc: BenchmarkAccumulator): OperationStats {\n if (acc.count === 0) return { count: 0, avgMs: 0, p95Ms: 0, p99Ms: 0, maxMs: 0 };\n\n const sorted = [...acc.samples].sort((a, b) => a - b);\n return {\n count: acc.count,\n avgMs: acc.totalMs / acc.count,\n p95Ms: percentile(sorted, 0.95),\n p99Ms: percentile(sorted, 0.99),\n maxMs: acc.maxMs,\n };\n}\n\nconst textEncoder = typeof TextEncoder !== 'undefined' ? new TextEncoder() : null;\n\ntype OpName =\n | 'track'\n | 'bloomAdd'\n | 'bloomCheck'\n | 'incrementTransition'\n | 'entropyComputation'\n | 'divergenceComputation';\n\n/**\n * Optional performance recorder for the SDK’s internal hot-path operations.\n *\n * Disabled by default (`enabled: false`) — all `now()` and `record()` calls\n * are no-ops when disabled, so there is zero overhead in production unless\n * callers explicitly opt in via `IntentManagerConfig.benchmark.enabled: true`.\n *\n * Exposes p95/p99 latency and a memory-footprint snapshot via `report()`.\n */\nexport class BenchmarkRecorder {\n readonly enabled: boolean;\n private readonly maxSamples: number;\n private readonly stats: Record<OpName, BenchmarkAccumulator>;\n\n constructor(config: BenchmarkConfig = {}) {\n this.enabled = config.enabled ?? false;\n this.maxSamples = config.maxSamples ?? DEFAULT_BENCHMARK_MAX_SAMPLES;\n this.stats = {\n track: createAccumulator(),\n bloomAdd: createAccumulator(),\n bloomCheck: createAccumulator(),\n incrementTransition: createAccumulator(),\n entropyComputation: createAccumulator(),\n divergenceComputation: createAccumulator(),\n };\n }\n\n now(): number {\n return this.enabled ? performance.now() : 0;\n }\n\n record(operation: OpName, startedAt: number): void {\n if (!this.enabled) return;\n recordSample(this.stats[operation], performance.now() - startedAt, this.maxSamples);\n }\n\n report(memoryFootprint: MemoryFootprintReport): PerformanceReport {\n return {\n benchmarkEnabled: this.enabled,\n track: toOperationStats(this.stats.track),\n bloomAdd: toOperationStats(this.stats.bloomAdd),\n bloomCheck: toOperationStats(this.stats.bloomCheck),\n incrementTransition: toOperationStats(this.stats.incrementTransition),\n entropyComputation: toOperationStats(this.stats.entropyComputation),\n divergenceComputation: toOperationStats(this.stats.divergenceComputation),\n memoryFootprint,\n };\n }\n\n serializedSizeBytes(payload: unknown): number {\n const asText = JSON.stringify(payload);\n return textEncoder ? textEncoder.encode(asText).byteLength : asText.length;\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/**\n * Isomorphic adapters for storage and timers.\n * --------------------------------------------------------\n * Allows the SDK to run safely in SSR environments (Next.js,\n * Nuxt, Remix, etc.) where `window` / `localStorage` are\n * not available at import time or at runtime.\n *\n * Browser implementations gracefully degrade to no-ops when\n * the DOM globals are absent.\n */\n\n/* ------------------------------------------------------------------ */\n/* Storage Adapter */\n/* ------------------------------------------------------------------ */\n\nexport interface StorageAdapter {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n}\n\n/**\n * Async storage adapter for environments where storage I/O is inherently\n * asynchronous (React Native AsyncStorage, Capacitor Preferences, IndexedDB\n * wrappers, etc.).\n *\n * Use `IntentManager.createAsync(config)` to initialize the engine with an\n * async backend — the factory awaits the initial `getItem` call before\n * constructing the engine, preserving the synchronous hot-path for `track()`.\n *\n * The synchronous `StorageAdapter` interface remains the default for\n * browser `localStorage`-backed use cases.\n */\nexport interface AsyncStorageAdapter {\n getItem(key: string): Promise<string | null>;\n setItem(key: string, value: string): Promise<void>;\n}\n\n/**\n * localStorage-backed adapter.\n * Falls back to no-ops when `window` or `localStorage` is unavailable\n * (e.g. SSR, Web Workers, or restrictive iframes).\n */\nexport class BrowserStorageAdapter implements StorageAdapter {\n getItem(key: string): string | null {\n if (typeof window === 'undefined' || !window.localStorage) return null;\n try {\n return window.localStorage.getItem(key);\n } catch {\n // SecurityError in sandboxed iframes / opaque origins\n return null;\n }\n }\n\n setItem(key: string, value: string): void {\n if (typeof window === 'undefined' || !window.localStorage) return;\n // QuotaExceededError / SecurityError are intentionally NOT caught here.\n // The caller (IntentManager.persist) wraps this in its own try/catch\n // so the error surfaces through the configured onError callback.\n window.localStorage.setItem(key, value);\n }\n}\n\n/* ------------------------------------------------------------------ */\n/* Timer Adapter */\n/* ------------------------------------------------------------------ */\n\n/**\n * Opaque handle returned by the timer adapter.\n * Bridges the gap between browser (number) and Node.js (Timeout) return types.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type TimerHandle = any;\n\nexport interface TimerAdapter {\n setTimeout(fn: () => void, delay: number): TimerHandle;\n clearTimeout(id: TimerHandle): void;\n now(): number;\n}\n\n/**\n * Timer adapter backed by the global `setTimeout` / `clearTimeout`.\n * Uses `globalThis` so it works in browsers, Node.js, Deno, Bun, and\n * Cloudflare Workers alike.\n */\nexport class BrowserTimerAdapter implements TimerAdapter {\n setTimeout(fn: () => void, delay: number): TimerHandle {\n if (typeof globalThis.setTimeout !== 'function') {\n // Edge-case: extremely minimal JS runtimes without timers.\n return 0 as TimerHandle;\n }\n return globalThis.setTimeout(fn, delay);\n }\n\n clearTimeout(id: TimerHandle): void {\n if (typeof globalThis.clearTimeout !== 'function') return;\n globalThis.clearTimeout(id);\n }\n\n now(): number {\n if (\n typeof globalThis.performance !== 'undefined' &&\n typeof globalThis.performance.now === 'function'\n ) {\n return globalThis.performance.now();\n }\n return Date.now();\n }\n}\n\n/* ------------------------------------------------------------------ */\n/* In-Memory Adapter (useful for tests / SSR) */\n/* ------------------------------------------------------------------ */\n\n/**\n * Simple in-memory storage adapter.\n * Handy for unit tests and server-side rendering where persistence is\n * neither needed nor available.\n */\nexport class MemoryStorageAdapter implements StorageAdapter {\n private readonly store = new Map<string, string>();\n\n getItem(key: string): string | null {\n return this.store.get(key) ?? null;\n }\n\n setItem(key: string, value: string): void {\n this.store.set(key, value);\n }\n}\n\n/* ------------------------------------------------------------------ */\n/* Lifecycle Adapter */\n/* ------------------------------------------------------------------ */\n\n/**\n * Abstracts browser DOM lifecycle events (page visibility) out of the core\n * engine so that the SDK can be used safely in React Native, Electron, and\n * server-side / SSR environments where `document` is absent.\n *\n * Implementations should call registered callbacks when the host environment\n * transitions between an active (\"resumed\") and an inactive (\"paused\") state.\n */\nexport interface LifecycleAdapter {\n /**\n * Register a callback to be invoked when the environment becomes inactive.\n * Returns an unsubscribe function that removes only this callback, leaving\n * any other registered callbacks untouched.\n */\n onPause(callback: () => void): () => void;\n /**\n * Register a callback to be invoked when the environment becomes active.\n * Returns an unsubscribe function that removes only this callback, leaving\n * any other registered callbacks untouched.\n */\n onResume(callback: () => void): () => void;\n /**\n * Optional: register a callback to be invoked on any user interaction\n * (mouse, keyboard, scroll, touch). Used by the idle-state detector.\n *\n * Implementations should throttle the callback internally (e.g. max once\n * per 1 000 ms) to avoid flooding the engine with high-frequency events.\n *\n * Returns an unsubscribe function that removes only this callback, or\n * `null` when the environment cannot deliver interaction events (e.g.\n * SSR, Node.js tests with a stubbed `window`).\n *\n * Backward-compatible — adapters that do not implement this method are\n * silently skipped and idle detection is disabled.\n */\n onInteraction?(callback: () => void): (() => void) | null;\n /**\n * Optional: register a callback to be invoked when the user signals exit\n * intent by moving the pointer above the viewport top edge (toward the\n * browser chrome / address bar).\n *\n * Implementations should only fire the callback when `MouseEvent.clientY <= 0`\n * so that normal in-page mouse movement is ignored.\n *\n * Returns an unsubscribe function that removes only this callback.\n *\n * Backward-compatible — adapters that do not implement this method are\n * silently skipped and exit-intent detection is disabled.\n */\n onExitIntent?(callback: () => void): () => void;\n /** Remove all event listeners and release resources held by this adapter. */\n destroy(): void;\n}\n\n/**\n * Lifecycle adapter backed by the Page Visibility API\n * (`document.visibilitychange`).\n *\n * Guards every `document` access with a `typeof document !== 'undefined'`\n * check so the class can be imported in SSR / Node.js / React Native\n * environments without throwing.\n *\n * Usage:\n * ```ts\n * const lifecycle = new BrowserLifecycleAdapter();\n * lifecycle.onPause(() => {\n * // e.g. flush pending work or persist state\n * });\n * lifecycle.onResume(() => {\n * // e.g. restart timers or resume work\n * });\n * // later…\n * lifecycle.destroy();\n * ```\n */\nexport class BrowserLifecycleAdapter implements LifecycleAdapter {\n private readonly pauseCallbacks: Array<() => void> = [];\n private readonly resumeCallbacks: Array<() => void> = [];\n private readonly interactionCallbacks: Array<() => void> = [];\n private readonly exitIntentCallbacks: Array<() => void> = [];\n private readonly handler: () => void;\n\n /** Tracks the DOM listeners registered for interaction throttling. */\n private interactionHandler: (() => void) | null = null;\n private interactionLastFired = 0;\n private static readonly INTERACTION_THROTTLE_MS = 1_000;\n private static readonly INTERACTION_EVENTS: ReadonlyArray<string> = [\n 'mousemove',\n 'scroll',\n 'touchstart',\n 'keydown',\n ];\n\n /** Handler for exit-intent mouseleave detection on document.documentElement. */\n private exitIntentHandler: ((e: MouseEvent) => void) | null = null;\n\n constructor() {\n this.handler = () => {\n if (typeof document === 'undefined') return;\n if (document.hidden) {\n for (const cb of this.pauseCallbacks) cb();\n } else {\n for (const cb of this.resumeCallbacks) cb();\n }\n };\n\n if (typeof document !== 'undefined') {\n document.addEventListener('visibilitychange', this.handler);\n }\n }\n\n onPause(callback: () => void): () => void {\n this.pauseCallbacks.push(callback);\n return () => {\n const idx = this.pauseCallbacks.indexOf(callback);\n if (idx !== -1) this.pauseCallbacks.splice(idx, 1);\n };\n }\n\n onResume(callback: () => void): () => void {\n this.resumeCallbacks.push(callback);\n return () => {\n const idx = this.resumeCallbacks.indexOf(callback);\n if (idx !== -1) this.resumeCallbacks.splice(idx, 1);\n };\n }\n\n onInteraction(callback: () => void): (() => void) | null {\n // When DOM event APIs are unavailable (SSR, Node.js tests with a\n // stubbed `window`), interaction tracking cannot function.\n // Return null to signal \"not supported\" so the coordinator skips\n // idle-check timer scheduling.\n if (typeof window === 'undefined' || typeof window.addEventListener !== 'function') {\n return null;\n }\n\n this.interactionCallbacks.push(callback);\n\n // Lazily attach DOM listeners on the first subscription.\n if (\n this.interactionHandler === null &&\n typeof window !== 'undefined' &&\n typeof window.addEventListener === 'function'\n ) {\n this.interactionHandler = () => {\n const now =\n typeof performance !== 'undefined' && typeof performance.now === 'function'\n ? performance.now()\n : Date.now();\n if (now - this.interactionLastFired < BrowserLifecycleAdapter.INTERACTION_THROTTLE_MS) {\n return;\n }\n this.interactionLastFired = now;\n for (const cb of this.interactionCallbacks) cb();\n };\n\n const opts: AddEventListenerOptions = { passive: true };\n for (const evt of BrowserLifecycleAdapter.INTERACTION_EVENTS) {\n window.addEventListener(evt, this.interactionHandler, opts);\n }\n }\n\n return () => {\n const idx = this.interactionCallbacks.indexOf(callback);\n if (idx !== -1) this.interactionCallbacks.splice(idx, 1);\n\n // Remove DOM listeners when the last subscriber unsubscribes.\n if (this.interactionCallbacks.length === 0) {\n this.teardownInteractionListeners();\n }\n };\n }\n\n /** Remove interaction DOM listeners if they are currently attached. */\n private teardownInteractionListeners(): void {\n if (\n this.interactionHandler !== null &&\n typeof window !== 'undefined' &&\n typeof window.removeEventListener === 'function'\n ) {\n for (const evt of BrowserLifecycleAdapter.INTERACTION_EVENTS) {\n window.removeEventListener(evt, this.interactionHandler);\n }\n }\n this.interactionHandler = null;\n }\n\n /**\n * Register a callback to be invoked when the user moves the pointer above\n * the top edge of the viewport (clientY <= 0), which typically indicates\n * they are heading toward the browser chrome / address bar — a reliable\n * proxy for exit intent on desktop.\n *\n * The listener is attached lazily on the first subscription so that the\n * adapter stays tree-shakeable in SSR / Node.js environments.\n */\n onExitIntent(callback: () => void): () => void {\n this.exitIntentCallbacks.push(callback);\n\n // Lazily attach the DOM listener on the first subscription.\n if (this.exitIntentHandler === null) {\n const root = typeof document !== 'undefined' ? document?.documentElement : null;\n if (root !== null && root !== undefined) {\n this.exitIntentHandler = (e: MouseEvent) => {\n if (e.clientY <= 0) {\n for (const cb of this.exitIntentCallbacks) cb();\n }\n };\n root.addEventListener('mouseleave', this.exitIntentHandler as EventListener);\n }\n }\n\n return () => {\n const idx = this.exitIntentCallbacks.indexOf(callback);\n if (idx !== -1) this.exitIntentCallbacks.splice(idx, 1);\n\n // Remove the DOM listener when the last subscriber unsubscribes.\n if (this.exitIntentCallbacks.length === 0) {\n this.teardownExitIntentListener();\n }\n };\n }\n\n /** Remove the exit-intent DOM listener if currently attached. */\n private teardownExitIntentListener(): void {\n if (this.exitIntentHandler !== null) {\n const root = typeof document !== 'undefined' ? document?.documentElement : null;\n if (root !== null && root !== undefined) {\n root.removeEventListener('mouseleave', this.exitIntentHandler as EventListener);\n }\n }\n this.exitIntentHandler = null;\n }\n\n destroy(): void {\n if (typeof document !== 'undefined') {\n document.removeEventListener('visibilitychange', this.handler);\n }\n this.teardownInteractionListeners();\n this.teardownExitIntentListener();\n this.pauseCallbacks.length = 0;\n this.resumeCallbacks.length = 0;\n this.interactionCallbacks.length = 0;\n this.exitIntentCallbacks.length = 0;\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/**\n * Pre-compiled patterns for dynamic path-segment detection.\n *\n * Both regexes are compiled **once at module load time** so that every\n * `normalizeRouteState()` call is allocation-free on the hot path.\n */\n\n/**\n * Matches a v4 UUID in its canonical 8-4-4-4-12 hex form.\n *\n * Version constraint: the third group starts with `4` (version bit).\n * Variant constraint: the fourth group starts with `8`, `9`, `a`, or `b`\n * (RFC 4122 variant bits).\n * Case-insensitive: both upper- and lower-case hex digits are accepted.\n */\nconst UUID_V4_RE = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/gi;\n\n/**\n * Matches a MongoDB ObjectID: exactly 24 hexadecimal characters forming a\n * complete word (no adjoining word characters on either side).\n *\n * The `\\b` word-boundary anchors prevent partial matches inside longer hex\n * strings (e.g. a 26-char string is not accidentally truncated).\n * Case-insensitive so both lowercase storage and uppercase display variants\n * are handled.\n */\nconst MONGO_ID_RE = /\\b[0-9a-f]{24}\\b/gi;\n\n/**\n * Matches a path segment that is a purely numeric integer ID with 4 or more\n * digits. The threshold of 4 avoids stripping common low-value segments\n * like `/page/2` (pagination) or `/step/3` (wizard steps).\n *\n * Pattern breakdown:\n * `\\/` — must be preceded by a slash (path separator)\n * `\\d{4,}` — four or more consecutive digits\n * `(?=\\/|$)` — followed by another slash or end-of-string (segment boundary)\n *\n * Global flag: multiple numeric segments in one path are all replaced.\n */\nconst NUMERIC_ID_RE = /\\/\\d{4,}(?=\\/|$)/g;\n\n/**\n * Normalizes a URL or route path to a canonical state key for use as an\n * `IntentManager` state label.\n *\n * Transformations applied in order:\n *\n * 1. **Strip query string** — everything from the first `?` onwards is removed.\n * 2. **Strip hash fragment** — everything from the first `#` onwards is removed.\n * 3. **Replace dynamic ID segments** — v4 UUIDs, 24-character hex MongoDB\n * ObjectIDs, and numeric IDs with 4+ digits found anywhere in the remaining\n * path are replaced with `:id`.\n * 4. **Remove trailing slash** — a single trailing `/` is removed so that\n * `/checkout/` and `/checkout` resolve to the same state. The bare root\n * path `/` is left unchanged.\n *\n * The function is **pure**: identical inputs always produce identical outputs\n * and it has no side-effects. All regex patterns are compiled once at module\n * load time so the per-call cost is limited to string scanning.\n *\n * @example\n * normalizeRouteState('/users/550e8400-e29b-41d4-a716-446655440000/profile?tab=bio#section')\n * // → '/users/:id/profile'\n *\n * @example\n * normalizeRouteState('/products/507f1f77bcf86cd799439011/reviews/')\n * // → '/products/:id/reviews'\n *\n * @example\n * normalizeRouteState('/user/12345/profile')\n * // → '/user/:id/profile'\n *\n * @example\n * normalizeRouteState('/checkout/')\n * // → '/checkout'\n *\n * @param url - A URL string or path, e.g. from `window.location.href` or\n * `window.location.pathname`.\n * @returns The normalized, canonical route state string.\n */\nexport function normalizeRouteState(url: string): string {\n // 1. Strip query string — slice at the first '?'\n const qIdx = url.indexOf('?');\n let path = qIdx !== -1 ? url.slice(0, qIdx) : url;\n\n // 2. Strip hash fragment — slice at the first '#'\n const hIdx = path.indexOf('#');\n if (hIdx !== -1) path = path.slice(0, hIdx);\n\n // 3. Replace v4 UUIDs first (more specific), then MongoDB ObjectIDs,\n // then numeric IDs (4+ digit path segments).\n // UUID replacement runs first because a UUID may contain 24-char hex\n // sub-sequences that would otherwise be caught by the ObjectID regex.\n path = path.replace(UUID_V4_RE, ':id').replace(MONGO_ID_RE, ':id').replace(NUMERIC_ID_RE, '/:id');\n\n // 4. Remove a single trailing slash, but preserve the bare root '/'.\n if (path.length > 1 && path.endsWith('/')) {\n path = path.slice(0, -1);\n }\n\n return path;\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/**\n * Laplace smoothing epsilon used for log-likelihood calculations.\n * Must be identical between calibration (intent-sdk-performance) and runtime (IntentManager).\n */\nexport const SMOOTHING_EPSILON = 0.01;\n\n/**\n * Minimum sliding window length before evaluating trajectory.\n * This \"warm-up\" allows the average log-likelihood to stabilize.\n * Must match the warm-up gate in IntentManager.evaluateTrajectory and\n * the calibration sampler in intent-sdk-performance.\n */\nexport const MIN_WINDOW_LENGTH = 16;\n\n/**\n * Maximum sliding window length (recentTrajectory cap).\n * Used as reference for variance scaling.\n * Must match the recentTrajectory cap in IntentManager and the calibration\n * sampler in intent-sdk-performance.\n */\nexport const MAX_WINDOW_LENGTH = 32;\n\n/**\n * Minimum number of outgoing transitions a state must have before entropy\n * evaluation is considered statistically meaningful.\n * Higher values prevent spurious entropy triggers on small samples.\n */\nexport const MIN_SAMPLE_TRANSITIONS = 10;\n\n/**\n * Upper bound on any single dwell-time or hidden-duration measurement (30 minutes).\n *\n * CPU suspend, laptop sleep, and OS hibernation cause the monotonic clock to\n * jump by hours while the Page Visibility API reports the tab as hidden. Any\n * raw delta that exceeds this threshold is almost certainly caused by the host\n * machine being suspended rather than genuine user behaviour and must never be\n * fed into the Welford variance accumulator or used to offset previousStateEnteredAt.\n *\n * When the threshold is breached the engine:\n * 1. Discards the inflated measurement — does NOT update Welford stats.\n * 2. Resets the dwell baseline to the current timestamp so the next\n * measurement starts from a clean epoch.\n * 3. Emits a `session_stale` diagnostic event for host-app observability.\n */\nexport const MAX_PLAUSIBLE_DWELL_MS = 1_800_000; // 30 minutes\n\n/**\n * Minimum tab-hidden duration before the engine considers the user a\n * \"comparison shopper\" and emits an `attention_return` event on resume.\n *\n * 15 seconds is long enough to filter out quick alt-tab / notification\n * glances while capturing users who navigated to a competitor tab.\n */\nexport const ATTENTION_RETURN_THRESHOLD_MS = 15_000; // 15 seconds\n\n/**\n * Duration of user inactivity (no mouse, keyboard, scroll, or touch events)\n * before the engine considers the user idle and emits a `user_idle` event.\n *\n * 2 minutes is a conservative default that avoids false positives from short\n * pauses (reading long content, watching embedded video) while still catching\n * users who genuinely walked away from their device.\n */\nexport const USER_IDLE_THRESHOLD_MS = 120_000; // 2 minutes\n\n/**\n * Interval between successive idle-state checks inside the LifecycleCoordinator.\n *\n * A 5-second polling cadence keeps CPU overhead negligible while ensuring the\n * `user_idle` event fires within 5 seconds of the actual threshold crossing.\n */\nexport const IDLE_CHECK_INTERVAL_MS = 5_000; // 5 seconds\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport type { IntentManagerConfig, MarkovGraphConfig } from '../types/events.js';\nimport type { EnginePolicy } from './policies/engine-policy.js';\nimport { SMOOTHING_EPSILON } from './constants.js';\n\n/**\n * Converts a one-tailed false positive rate to a Z-score threshold via the\n * standard inverse-normal CDF: z = Φ⁻¹(1 − fpr).\n *\n * Uses the rational approximation by Beasley & Springer (1977) — accurate to\n * within 4.5 × 10⁻⁴ across (0, 0.5], which is far tighter than the asymptotic\n * `√(−2 ln fpr)` approximation (≈ 36 % error at fpr = 0.05).\n * Finite inputs outside [0.001, 0.5] are silently clamped.\n * Non-finite inputs (NaN, ±Infinity) return NaN; callers must guard with\n * `Number.isFinite` and fall back to a sensible default.\n *\n * Bundle-size optimised: six scalar constants, no lookup tables.\n */\nfunction fprToZScore(fpr: number): number {\n if (!Number.isFinite(fpr)) return NaN;\n const p = Math.min(0.5, Math.max(0.001, fpr));\n const t = Math.sqrt(-2.0 * Math.log(p));\n return (\n t -\n (2.515517 + t * (0.802853 + t * 0.010328)) /\n (1.0 + t * (1.432788 + t * (0.189269 + t * 0.001308)))\n );\n}\n\n/**\n * Strongly-typed internal options object produced by normalizing an\n * `IntentManagerConfig`. Every field has a concrete, non-optional value —\n * the constructor can use them directly without further null-coalescing.\n *\n * This covers **only** the config-precedence / default / clamping logic that\n * previously lived inside the `IntentManager` constructor. Fields that are\n * pure pass-through (e.g. `timer`, `storage`, `benchmark`, `onError`,\n * `baseline`, `lifecycleAdapter`, `asyncStorage`) are intentionally omitted\n * because they carry no defaulting or merging logic of their own.\n *\n * @internal Not part of the public package API. Field names and defaults may\n * change across any release without a semver-major bump.\n */\nexport interface ResolvedIntentManagerOptions {\n /* ── Scalar flags ──────────────────────────────────────────────────────── */\n botProtection: boolean;\n dwellTimeEnabled: boolean;\n crossTabSync: boolean;\n\n /* ── Holdout ───────────────────────────────────────────────────────────── */\n /** Clamped to [0, 100]. */\n holdoutPercent: number;\n\n /* ── Graph config (merged from top-level aliases + nested graph) ──────── */\n graphConfig: MarkovGraphConfig;\n\n /**\n * Resolved Laplace smoothing epsilon for trajectory scoring.\n * Falls back to `SMOOTHING_EPSILON` when the configured value is not\n * a finite positive number.\n */\n trajectorySmoothingEpsilon: number;\n\n /* ── Persistence ───────────────────────────────────────────────────────── */\n storageKey: string;\n persistDebounceMs: number;\n persistThrottleMs: number;\n\n /* ── Signal engine ─────────────────────────────────────────────────────── */\n eventCooldownMs: number;\n dwellTimeMinSamples: number;\n dwellTimeZScoreThreshold: number;\n enableBigrams: boolean;\n bigramFrequencyThreshold: number;\n driftMaxAnomalyRate: number;\n driftEvaluationWindowMs: number;\n hesitationCorrelationWindowMs: number;\n plugins: EnginePolicy[];\n}\n\n/**\n * Pure function that normalizes an external `IntentManagerConfig` into a\n * fully-resolved `ResolvedIntentManagerOptions`.\n *\n * All precedence, default-value, and clamping rules are centralised here so\n * that the `IntentManager` constructor becomes pure wiring.\n *\n * **Precedence rules (unchanged from original):**\n * - `config.baselineMeanLL` wins over `config.graph.baselineMeanLL`\n * - `config.baselineStdLL` wins over `config.graph.baselineStdLL`\n * - `config.smoothingAlpha` wins over `config.graph.smoothingAlpha`\n *\n * **Clamping rules:**\n * - `holdoutConfig.percentage` is clamped to [0, 100].\n * - `graphConfig.smoothingEpsilon` must be a finite positive number; otherwise\n * the compile-time constant `SMOOTHING_EPSILON` is used.\n *\n * @internal Not part of the public package API.\n */\nexport function buildIntentManagerOptions(\n config: IntentManagerConfig = {},\n): ResolvedIntentManagerOptions {\n // ── Scalar flags ────────────────────────────────────────────────────────\n const botProtection = config.botProtection ?? true;\n const dwellTimeEnabled = config.dwellTime?.enabled ?? false;\n const crossTabSync = config.crossTabSync === true;\n\n // ── Holdout — clamped to [0, 100] ──────────────────────────────────────\n const rawHoldoutPct = config.holdoutConfig?.percentage;\n const holdoutPercent = Number.isFinite(rawHoldoutPct)\n ? Math.min(100, Math.max(0, rawHoldoutPct as number))\n : (rawHoldoutPct ?? 0);\n\n // ── Merge top-level convenience aliases into the nested graph config ────\n // Top-level fields take precedence when both are supplied.\n // targetFPR overrides divergenceThreshold when present.\n const graphFPR = config.graph?.targetFPR;\n const graphConfig: MarkovGraphConfig = {\n ...config.graph,\n baselineMeanLL: config.baselineMeanLL ?? config.graph?.baselineMeanLL,\n baselineStdLL: config.baselineStdLL ?? config.graph?.baselineStdLL,\n smoothingAlpha: config.smoothingAlpha ?? config.graph?.smoothingAlpha,\n divergenceThreshold: Number.isFinite(graphFPR)\n ? fprToZScore(graphFPR as number)\n : config.graph?.divergenceThreshold,\n };\n\n // ── Trajectory smoothing epsilon ────────────────────────────────────────\n const configuredSmoothing = graphConfig.smoothingEpsilon;\n const trajectorySmoothingEpsilon =\n typeof configuredSmoothing === 'number' &&\n Number.isFinite(configuredSmoothing) &&\n configuredSmoothing > 0\n ? configuredSmoothing\n : SMOOTHING_EPSILON;\n\n // ── Persistence ─────────────────────────────────────────────────────────\n const storageKey = config.storageKey ?? 'passive-intent';\n\n const rawPersistDebounce = config.persistDebounceMs;\n const persistDebounceMs =\n Number.isFinite(rawPersistDebounce) && (rawPersistDebounce as number) >= 0\n ? Math.floor(rawPersistDebounce as number)\n : 2000;\n\n const rawPersistThrottle = config.persistThrottleMs;\n const persistThrottleMs =\n Number.isFinite(rawPersistThrottle) && (rawPersistThrottle as number) >= 0\n ? Math.floor(rawPersistThrottle as number)\n : 0;\n\n // ── Signal engine ───────────────────────────────────────────────────────\n const rawEventCooldown = config.eventCooldownMs;\n const eventCooldownMs =\n Number.isFinite(rawEventCooldown) && (rawEventCooldown as number) >= 0\n ? Math.floor(rawEventCooldown as number)\n : 0;\n\n const rawDwellMinSamples = config.dwellTime?.minSamples;\n const dwellTimeMinSamples =\n Number.isFinite(rawDwellMinSamples) && (rawDwellMinSamples as number) >= 1\n ? Math.floor(rawDwellMinSamples as number)\n : 10;\n\n const dwellFPR = config.dwellTime?.targetFPR;\n const rawDwellZScore = config.dwellTime?.zScoreThreshold;\n const dwellTimeZScoreThreshold = Number.isFinite(dwellFPR)\n ? fprToZScore(dwellFPR as number)\n : Number.isFinite(rawDwellZScore) && (rawDwellZScore as number) > 0\n ? (rawDwellZScore as number)\n : 2.5;\n\n const enableBigrams = config.enableBigrams ?? false;\n\n const rawBigramThreshold = config.bigramFrequencyThreshold;\n const bigramFrequencyThreshold =\n Number.isFinite(rawBigramThreshold) && (rawBigramThreshold as number) >= 1\n ? Math.floor(rawBigramThreshold as number)\n : 5;\n\n const rawDriftMaxRate = config.driftProtection?.maxAnomalyRate;\n const driftMaxAnomalyRate = Number.isFinite(rawDriftMaxRate)\n ? Math.min(1, Math.max(0, rawDriftMaxRate as number))\n : 0.4;\n\n const rawDriftWindowMs = config.driftProtection?.evaluationWindowMs;\n const driftEvaluationWindowMs =\n Number.isFinite(rawDriftWindowMs) && (rawDriftWindowMs as number) > 0\n ? Math.floor(rawDriftWindowMs as number)\n : 300_000;\n\n const rawHesitationMs = config.hesitationCorrelationWindowMs;\n const hesitationCorrelationWindowMs =\n Number.isFinite(rawHesitationMs) && (rawHesitationMs as number) >= 0\n ? Math.floor(rawHesitationMs as number)\n : 30_000;\n\n return {\n botProtection,\n dwellTimeEnabled,\n crossTabSync,\n holdoutPercent,\n graphConfig,\n trajectorySmoothingEpsilon,\n storageKey,\n persistDebounceMs,\n persistThrottleMs,\n eventCooldownMs,\n dwellTimeMinSamples,\n dwellTimeZScoreThreshold,\n enableBigrams,\n bigramFrequencyThreshold,\n driftMaxAnomalyRate,\n driftEvaluationWindowMs,\n hesitationCorrelationWindowMs,\n plugins: config.plugins ?? [],\n };\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\ntype Listener<T> = (payload: T) => void;\n\n/**\n * Minimal, generic typed event emitter used by IntentManager to decouple\n * event production from consumption.\n *\n * - `on()` returns an unsubscribe function for clean teardown in SPA lifecycles.\n * - `emit()` iterates the listener set synchronously; listeners are called in\n * insertion order.\n * - `removeAll()` is used by `IntentManager.destroy()` to prevent memory leaks\n * when the instance is torn down.\n *\n * The class is intentionally kept framework-agnostic and dependency-free so it\n * can be extracted or tested in isolation without any engine context.\n */\nexport class EventEmitter<Events extends object> {\n private listeners = new Map<keyof Events, Set<Listener<any>>>();\n\n on<K extends keyof Events>(event: K, listener: Listener<Events[K]>): () => void {\n const set = this.listeners.get(event) ?? new Set<Listener<Events[K]>>();\n set.add(listener);\n this.listeners.set(event, set as Set<Listener<any>>);\n\n return () => {\n set.delete(listener);\n if (set.size === 0) this.listeners.delete(event);\n };\n }\n\n emit<K extends keyof Events>(event: K, payload: Events[K]): void {\n const set = this.listeners.get(event);\n if (!set) return;\n set.forEach((listener) => listener(payload));\n }\n\n removeAll(): void {\n this.listeners.clear();\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/**\n * Heuristic bot detector based on inter-event timing statistics.\n *\n * Two scoring criteria are evaluated over a sliding window of the last\n * `BOT_DETECTION_WINDOW` (10) `track()` call timestamps:\n *\n * 1. **Speed criterion** — each consecutive delta < `BOT_MIN_DELTA_MS` (50 ms)\n * increments the score. Real users rarely navigate faster than 50 ms\n * per page; automation tools do it consistently.\n *\n * 2. **Variance criterion** — if the variance of all deltas in the window\n * falls below `BOT_MAX_VARIANCE` (100 ms²), the score gains 1 extra point.\n * Human timing has natural jitter; bots that pace calls with `setInterval`\n * produce near-zero variance even if each individual call is plausible.\n *\n * The `isSuspectedBot` flag flips to `true` when the combined score ≥\n * `BOT_SCORE_THRESHOLD` (5) and stays `true` until enough slow, high-\n * variance calls push the score back below the threshold.\n *\n * **No false negatives by design:** if bot protection is disabled at the\n * `IntentManager` level, this class is never called. When enabled, the\n * tradeoff is a small false-positive risk for users on very fast connections\n * or programmatic navigation (e.g. router `push()` in rapid succession).\n */\n\nconst BOT_DETECTION_WINDOW = 10;\n/** Minimum realistic human inter-navigation delta in milliseconds. */\nconst BOT_MIN_DELTA_MS = 50;\n/** Maximum variance (ms²) for the timing-uniformity criterion. */\nconst BOT_MAX_VARIANCE = 100;\n/** Combined score threshold above which the session is classified as a bot. */\nconst BOT_SCORE_THRESHOLD = 5;\n\nexport class EntropyGuard {\n private isSuspectedBot = false;\n private readonly trackTimestamps: number[] = new Array(BOT_DETECTION_WINDOW).fill(0);\n private trackTimestampIndex = 0;\n private trackTimestampCount = 0;\n\n record(timestamp: number): { suspected: boolean; transitionedToBot: boolean } {\n this.trackTimestamps[this.trackTimestampIndex] = timestamp;\n this.trackTimestampIndex = (this.trackTimestampIndex + 1) % BOT_DETECTION_WINDOW;\n if (this.trackTimestampCount < BOT_DETECTION_WINDOW) {\n this.trackTimestampCount += 1;\n }\n\n const previous = this.isSuspectedBot;\n this.evaluate();\n return {\n suspected: this.isSuspectedBot,\n transitionedToBot: this.isSuspectedBot && !previous,\n };\n }\n\n get suspected(): boolean {\n return this.isSuspectedBot;\n }\n\n private evaluate(): void {\n const count = this.trackTimestampCount;\n if (count < 3) return;\n\n let windowBotScore = 0;\n const deltas: number[] = [];\n\n // Ring-buffer oldest entry: if the window is not yet full, oldest is index 0;\n // otherwise it's the slot that was written next (trackTimestampIndex wraps around).\n const oldestIndex = count < BOT_DETECTION_WINDOW ? 0 : this.trackTimestampIndex;\n\n for (let i = 0; i < count - 1; i += 1) {\n const currIdx = (oldestIndex + i) % BOT_DETECTION_WINDOW;\n const nextIdx = (oldestIndex + i + 1) % BOT_DETECTION_WINDOW;\n const delta = this.trackTimestamps[nextIdx] - this.trackTimestamps[currIdx];\n deltas.push(delta);\n if (delta >= 0 && delta < BOT_MIN_DELTA_MS) {\n windowBotScore += 1;\n }\n }\n\n if (deltas.length >= 2) {\n let mean = 0;\n for (let i = 0; i < deltas.length; i += 1) {\n mean += deltas[i];\n }\n mean /= deltas.length;\n\n let variance = 0;\n for (let i = 0; i < deltas.length; i += 1) {\n const d = deltas[i] - mean;\n variance += d * d;\n }\n variance /= deltas.length;\n\n // variance is always ≥ 0 (sum of squares / count), so only the upper\n // bound matters: low variance means unnaturally regular timing.\n if (variance < BOT_MAX_VARIANCE) {\n windowBotScore += 1;\n }\n }\n\n this.isSuspectedBot = windowBotScore >= BOT_SCORE_THRESHOLD;\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/**\n * Welford's online algorithm state for a single numeric stream.\n *\n * Maintains mean and variance in a single pass without storing the raw\n * samples, making it O(1) per update and O(1) in memory regardless of\n * session length. All three fields are required to resume a running\n * computation (e.g. across persisted checkpoints).\n *\n * Reference: Welford, B.P. (1962), \"Note on a method for calculating\n * corrected sums of squares and products\", Technometrics 4(3):419–420.\n */\nexport interface DwellStats {\n /** Number of observations recorded so far. */\n count: number;\n /** Running arithmetic mean of dwell times in milliseconds. */\n meanMs: number;\n /**\n * Welford’s running sum of squared deviations from the mean.\n * Divide by `count` (biased) or `count - 1` (unbiased Bessel-corrected)\n * to obtain variance. `dwellStd()` uses the biased estimator intentionally\n * because we care about the current session’s distribution, not an\n * unbiased population estimate.\n */\n m2: number;\n}\n\n/**\n * Update a `DwellStats` accumulator with a new observation using\n * Welford’s algorithm. Creates a fresh accumulator if `current` is\n * undefined (i.e. first observation for a state).\n */\nexport function updateDwellStats(current: DwellStats | undefined, dwellMs: number): DwellStats {\n const previousCount = current?.count ?? 0;\n const previousMean = current?.meanMs ?? 0;\n const previousM2 = current?.m2 ?? 0;\n const count = previousCount + 1;\n const delta = dwellMs - previousMean;\n const meanMs = previousMean + delta / count;\n const delta2 = dwellMs - meanMs;\n const m2 = previousM2 + delta * delta2;\n return { count, meanMs, m2 };\n}\n\n/**\n * Biased population standard deviation derived from Welford state.\n * Returns 0 when fewer than 2 samples have been recorded (no meaningful\n * spread can be computed from a single data point).\n */\nexport function dwellStd(stats: DwellStats): number {\n if (stats.count < 2) return 0;\n return Math.sqrt(stats.m2 / stats.count);\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/**\n * AnomalyDispatcher — the single point that converts evaluator decisions\n * into observable side-effects.\n *\n * Responsibilities:\n * 1. **Cooldown policy** — suppresses events that fire more frequently than\n * `eventCooldownMs` per event type.\n * 2. **Holdout suppression** — events are never forwarded to the emitter\n * when `assignmentGroup === 'control'`.\n * 3. **Telemetry accounting** — `anomaliesFired` is incremented once per\n * dispatched decision that passes the cooldown gate.\n * 4. **Drift protection** — `driftPolicy.recordAnomaly()` is called for\n * every `TrajectoryDecision` *before* the cooldown check, preserving the\n * original semantics: drift accounting is done on the raw signal, not on\n * the rate-limited emission.\n * 5. **Hesitation correlation** — bookkeeping timestamps are updated after a\n * successful dispatch, and `hesitation_detected` is emitted whenever\n * both a trajectory anomaly and a positive-z-score dwell anomaly fall\n * within `hesitationCorrelationWindowMs`.\n *\n * All of the above were previously scattered across `SignalEngine`. Moving\n * them here lets the evaluator methods return pure decision values and keeps\n * the dispatcher as the only component that touches the emitter or mutates\n * any session state.\n */\n\nimport type { EventEmitter } from './event-emitter.js';\nimport type { IntentEventMap } from '../types/events.js';\nimport type { TimerAdapter } from '../adapters.js';\nimport type { DriftProtectionPolicy } from './policies/drift-protection-policy.js';\nimport type { AnomalyDecision } from './anomaly-decisions.js';\n\n/** Compile-time exhaustiveness check. */\nfunction assertNever(x: never): never {\n throw new Error(`Unhandled AnomalyDecision kind: ${(x as { kind: string }).kind}`);\n}\n\n/**\n * Structural view of the event emitter used by the dispatcher.\n *\n * This is intentionally minimal so that external consumers do not need access\n * to the concrete EventEmitter class, only to an object with the required\n * methods.\n */\nexport interface AnomalyEventEmitter {\n emit: EventEmitter<IntentEventMap>['emit'];\n on: EventEmitter<IntentEventMap>['on'];\n removeAll: EventEmitter<IntentEventMap>['removeAll'];\n}\n\n/**\n * Structural view of the drift-protection policy required by the dispatcher.\n */\nexport interface DriftProtectionPolicyLike {\n recordAnomaly: DriftProtectionPolicy['recordAnomaly'];\n readonly isDrifted: DriftProtectionPolicy['isDrifted'];\n readonly baselineStatus: DriftProtectionPolicy['baselineStatus'];\n}\n\nexport interface AnomalyDispatcherConfig {\n emitter: AnomalyEventEmitter;\n timer: TimerAdapter;\n assignmentGroup: 'treatment' | 'control';\n eventCooldownMs: number;\n hesitationCorrelationWindowMs: number;\n driftPolicy: DriftProtectionPolicyLike;\n}\n\nexport class AnomalyDispatcher {\n private readonly emitter: AnomalyEventEmitter;\n private readonly timer: TimerAdapter;\n private readonly assignmentGroup: 'treatment' | 'control';\n private readonly eventCooldownMs: number;\n private readonly hesitationCorrelationWindowMs: number;\n private readonly driftPolicy: DriftProtectionPolicyLike;\n\n /* Cooldown gating per event type */\n private readonly lastEmittedAt: Record<\n 'high_entropy' | 'trajectory_anomaly' | 'dwell_time_anomaly',\n number\n > = {\n high_entropy: -Infinity,\n trajectory_anomaly: -Infinity,\n dwell_time_anomaly: -Infinity,\n };\n\n /* Hesitation correlation state */\n private lastTrajectoryAnomalyAt = -Infinity;\n private lastTrajectoryAnomalyZScore = 0;\n private lastDwellAnomalyAt = -Infinity;\n private lastDwellAnomalyZScore = 0;\n private lastDwellAnomalyState = '';\n\n /* Session-scoped anomaly counter */\n private anomaliesFiredInternal = 0;\n\n constructor(config: AnomalyDispatcherConfig) {\n this.emitter = config.emitter;\n this.timer = config.timer;\n this.assignmentGroup = config.assignmentGroup;\n this.eventCooldownMs = config.eventCooldownMs;\n this.hesitationCorrelationWindowMs = config.hesitationCorrelationWindowMs;\n this.driftPolicy = config.driftPolicy;\n }\n\n /* ================================================================== */\n /* Telemetry getter */\n /* ================================================================== */\n\n get anomaliesFired(): number {\n return this.anomaliesFiredInternal;\n }\n\n /* ================================================================== */\n /* Dispatch */\n /* ================================================================== */\n\n /**\n * Apply cooldown policy, holdout suppression, telemetry increment, and all\n * emitter side-effects for a single evaluator decision.\n *\n * Passing `null` is a no-op and is the normal case when no anomaly was\n * detected on the current transition.\n *\n * @param decision - The decision returned by an evaluator, or `null`.\n */\n dispatch(decision: AnomalyDecision | null): void {\n if (decision === null) return;\n\n // ── Drift accounting (trajectory only, before cooldown) ─────────────────\n // Must mirror the original semantics: every detected trajectory anomaly\n // counts against the drift-protection window, regardless of whether the\n // event is actually emitted.\n if (decision.kind === 'trajectory_anomaly') {\n this.driftPolicy.recordAnomaly();\n }\n\n // ── Cooldown gate ─────────────────────────────────────────────────────────\n const now = this.timer.now();\n if (\n this.eventCooldownMs > 0 &&\n now - this.lastEmittedAt[decision.kind] < this.eventCooldownMs\n ) {\n return;\n }\n\n // ── Update cooldown timestamp ─────────────────────────────────────────────\n this.lastEmittedAt[decision.kind] = now;\n\n // ── Telemetry increment ───────────────────────────────────────────────────\n this.anomaliesFiredInternal += 1;\n\n // ── Holdout suppression + emission ───────────────────────────────────────\n if (this.assignmentGroup !== 'control') {\n switch (decision.kind) {\n case 'high_entropy':\n this.emitter.emit('high_entropy', decision.payload);\n break;\n case 'trajectory_anomaly':\n this.emitter.emit('trajectory_anomaly', decision.payload);\n break;\n case 'dwell_time_anomaly':\n this.emitter.emit('dwell_time_anomaly', decision.payload);\n break;\n default:\n assertNever(decision);\n }\n }\n\n // ── Hesitation correlation bookkeeping ────────────────────────────────────\n if (decision.kind === 'trajectory_anomaly') {\n this.lastTrajectoryAnomalyAt = now;\n this.lastTrajectoryAnomalyZScore = decision.payload.zScore;\n this.maybeEmitHesitation(now);\n } else if (decision.kind === 'dwell_time_anomaly' && decision.isPositiveZScore) {\n this.lastDwellAnomalyAt = now;\n this.lastDwellAnomalyZScore = decision.payload.zScore;\n this.lastDwellAnomalyState = decision.payload.state;\n this.maybeEmitHesitation(now);\n }\n }\n\n /* ================================================================== */\n /* Hesitation Correlation (private) */\n /* ================================================================== */\n\n private maybeEmitHesitation(now: number): void {\n const correlated =\n now - this.lastTrajectoryAnomalyAt < this.hesitationCorrelationWindowMs &&\n now - this.lastDwellAnomalyAt < this.hesitationCorrelationWindowMs;\n\n if (!correlated) return;\n\n // Consume both timestamps so the same pair cannot trigger twice.\n this.lastTrajectoryAnomalyAt = -Infinity;\n this.lastDwellAnomalyAt = -Infinity;\n\n if (this.assignmentGroup !== 'control') {\n this.emitter.emit('hesitation_detected', {\n state: this.lastDwellAnomalyState,\n trajectoryZScore: this.lastTrajectoryAnomalyZScore,\n dwellZScore: this.lastDwellAnomalyZScore,\n });\n }\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport type { BenchmarkRecorder } from '../performance-instrumentation.js';\nimport type { TimerAdapter } from '../adapters.js';\nimport { MarkovGraph } from '../core/markov.js';\nimport { EntropyGuard } from './entropy-guard.js';\nimport { dwellStd, updateDwellStats } from './dwell.js';\nimport type { DwellStats } from './dwell.js';\nimport { EventEmitter } from './event-emitter.js';\nimport type { AnomalyEventEmitter, DriftProtectionPolicyLike } from './anomaly-dispatcher.js';\nimport { MIN_SAMPLE_TRANSITIONS, MIN_WINDOW_LENGTH, MAX_WINDOW_LENGTH } from './constants.js';\nimport type { PassiveIntentTelemetry } from '../types/events.js';\nimport { AnomalyDispatcher } from './anomaly-dispatcher.js';\nimport type {\n AnomalyDecision,\n EntropyDecision,\n TrajectoryDecision,\n DwellDecision,\n} from './anomaly-decisions.js';\n\nexport { EventEmitter };\n\nfunction getConfidence(sampleSize: number): 'low' | 'medium' | 'high' {\n if (sampleSize < 10) return 'low';\n if (sampleSize < 30) return 'medium';\n return 'high';\n}\n/**\n * Configuration surface for SignalEngine.\n * All values are resolved and defaulted by IntentManager before being passed in.\n */\nexport interface SignalEngineConfig {\n graph: MarkovGraph;\n baseline: MarkovGraph | null;\n timer: TimerAdapter;\n benchmark: BenchmarkRecorder;\n emitter: AnomalyEventEmitter;\n assignmentGroup: 'treatment' | 'control';\n eventCooldownMs: number;\n dwellTimeMinSamples: number;\n dwellTimeZScoreThreshold: number;\n hesitationCorrelationWindowMs: number;\n trajectorySmoothingEpsilon: number;\n /** Drift protection policy — owns the rolling evaluation window and drifted flag. */\n driftPolicy: DriftProtectionPolicyLike;\n}\n\n/**\n * SignalEngine — pure computation kernel for all anomaly signals.\n *\n * ## Responsibilities\n * Each public `evaluate*` method is a **pure evaluator**: it reads engine state\n * and returns a typed decision object (or `null`), but never emits events,\n * mutates cooldown timestamps, or touches telemetry counters.\n *\n * Owns:\n * - `EntropyGuard` — bot-detection sliding window\n * - Per-state Welford accumulators for dwell-time statistics\n * - Session-scoped `transitionsEvaluated` counter\n *\n * ## What it does NOT own\n * All side-effects have been moved to `AnomalyDispatcher`, which is composed\n * internally and exposed through `dispatch()`:\n * - Cooldown gating per event type\n * - Holdout (control-group) suppression\n * - `anomaliesFired` telemetry increment\n * - Drift-protection `recordAnomaly()` calls\n * - Emitter calls for `high_entropy`, `trajectory_anomaly`, `dwell_time_anomaly`\n * - Hesitation correlation and `hesitation_detected` emission\n *\n * ## Usage pattern\n * ```ts\n * signalEngine.dispatch(signalEngine.evaluateEntropy(state));\n * signalEngine.dispatch(signalEngine.evaluateTrajectory(from, to, trajectory));\n * signalEngine.dispatch(signalEngine.evaluateDwellTime(state, dwellMs));\n * ```\n *\n * **Do not add side-effects to evaluator methods.** If a new signal type\n * requires emission or shared-state mutation, add a new `evaluate*` that\n * returns a decision type and handle it inside `AnomalyDispatcher.dispatch()`.\n *\n * IntentManager passes already-resolved config values and read-only trajectory\n * slices into each evaluation method; no I/O occurs here.\n */\nexport class SignalEngine {\n private readonly graph: MarkovGraph;\n private readonly baseline: MarkovGraph | null;\n private readonly benchmark: BenchmarkRecorder;\n private readonly dwellTimeMinSamples: number;\n private readonly dwellTimeZScoreThreshold: number;\n private readonly trajectorySmoothingEpsilon: number;\n private readonly driftPolicy: DriftProtectionPolicyLike;\n\n /* Bot detection */\n private readonly entropyGuard = new EntropyGuard();\n\n /* Dwell-time Welford accumulators — session-scoped, never persisted */\n private readonly dwellStats = new Map<string, DwellStats>();\n\n /* Dispatcher — owns cooldown, hesitation, telemetry, and emitter side-effects */\n private readonly dispatcher: AnomalyDispatcher;\n\n /* Session-scoped transition counter */\n private transitionsEvaluatedInternal = 0;\n\n constructor(config: SignalEngineConfig) {\n this.graph = config.graph;\n this.baseline = config.baseline;\n this.benchmark = config.benchmark;\n this.dwellTimeMinSamples = config.dwellTimeMinSamples;\n this.dwellTimeZScoreThreshold = config.dwellTimeZScoreThreshold;\n this.trajectorySmoothingEpsilon = config.trajectorySmoothingEpsilon;\n this.driftPolicy = config.driftPolicy;\n\n this.dispatcher = new AnomalyDispatcher({\n emitter: config.emitter,\n timer: config.timer,\n assignmentGroup: config.assignmentGroup,\n eventCooldownMs: config.eventCooldownMs,\n hesitationCorrelationWindowMs: config.hesitationCorrelationWindowMs,\n driftPolicy: config.driftPolicy,\n });\n }\n\n /* ================================================================== */\n /* Telemetry Getters */\n /* ================================================================== */\n\n get suspected(): boolean {\n return this.entropyGuard.suspected;\n }\n\n get transitionsEvaluated(): number {\n return this.transitionsEvaluatedInternal;\n }\n\n get anomaliesFired(): number {\n return this.dispatcher.anomaliesFired;\n }\n\n get isBaselineDrifted(): boolean {\n return this.driftPolicy.isDrifted;\n }\n\n get baselineStatus(): PassiveIntentTelemetry['baselineStatus'] {\n return this.driftPolicy.baselineStatus;\n }\n\n /* ================================================================== */\n /* Bot Protection */\n /* ================================================================== */\n\n /**\n * Record a `track()` timestamp into the EntropyGuard and return bot state.\n * If the guard transitions to suspected-bot, IntentManager emits `bot_detected`.\n */\n recordBotCheck(now: number): { suspected: boolean; transitionedToBot: boolean } {\n return this.entropyGuard.record(now);\n }\n\n /* ================================================================== */\n /* Transition Accounting */\n /* ================================================================== */\n\n /**\n * Increment the session-scoped transition counter.\n *\n * Bigram accounting was moved to `BigramPolicy.onTransition()` as part of\n * the policy refactor; this method's sole responsibility is updating the\n * `transitionsEvaluated` telemetry counter.\n */\n recordTransition(_from: string, _to: string, _trajectory: readonly string[]): void {\n this.transitionsEvaluatedInternal += 1;\n // Bigram accounting is now handled by BigramPolicy.onTransition().\n }\n\n /* ================================================================== */\n /* Dispatch delegation */\n /* ================================================================== */\n\n /**\n * Forward a decision produced by any evaluator to the AnomalyDispatcher.\n * Passing `null` is a safe no-op and is the common case when no anomaly\n * was detected.\n */\n dispatch(decision: AnomalyDecision | null): void {\n this.dispatcher.dispatch(decision);\n }\n\n /* ================================================================== */\n /* Entropy Evaluation */\n /* ================================================================== */\n\n /**\n * Evaluate the entropy of the current state and return a decision when an\n * anomaly is detected, or `null` when the state is normal or below the\n * minimum-sample threshold.\n *\n * This method is a **pure evaluator**: it reads state but performs no\n * side-effects. Call `dispatch(evaluateEntropy(state))` to apply\n * cooldown, holdout suppression, and emission.\n */\n evaluateEntropy(state: string): EntropyDecision | null {\n const start = this.benchmark.now();\n\n if (this.entropyGuard.suspected) {\n this.benchmark.record('entropyComputation', start);\n return null;\n }\n\n if (this.graph.rowTotal(state) < MIN_SAMPLE_TRANSITIONS) {\n this.benchmark.record('entropyComputation', start);\n return null;\n }\n\n const entropy = this.graph.entropyForState(state);\n const normalizedEntropy = this.graph.normalizedEntropyForState(state);\n\n this.benchmark.record('entropyComputation', start);\n\n if (normalizedEntropy >= this.graph.highEntropyThreshold) {\n return { kind: 'high_entropy', payload: { state, entropy, normalizedEntropy } };\n }\n\n return null;\n }\n\n /* ================================================================== */\n /* Trajectory Anomaly Detection */\n /* ================================================================== */\n\n /**\n * Evaluate the current trajectory against the baseline graph and return a\n * `TrajectoryDecision` when a z-score (or raw LL) anomaly is detected, or\n * `null` when the trajectory is normal or any precondition is unmet.\n *\n * This method is a **pure evaluator**: it reads state but performs no\n * side-effects. Drift accounting (`driftPolicy.recordAnomaly()`) is\n * intentionally deferred to `AnomalyDispatcher.dispatch()` where it is\n * applied *before* the cooldown check, preserving the original semantics.\n *\n * @param from Departing state of the most recent transition.\n * @param to Arriving state of the most recent transition.\n * @param trajectory Read-only snapshot of the sliding trajectory window.\n */\n evaluateTrajectory(\n from: string,\n to: string,\n trajectory: readonly string[],\n ): TrajectoryDecision | null {\n const start = this.benchmark.now();\n\n if (this.driftPolicy.isDrifted) {\n this.benchmark.record('divergenceComputation', start);\n return null;\n }\n\n if (this.entropyGuard.suspected) {\n this.benchmark.record('divergenceComputation', start);\n return null;\n }\n\n if (trajectory.length < MIN_WINDOW_LENGTH) {\n this.benchmark.record('divergenceComputation', start);\n return null;\n }\n\n if (!this.baseline) {\n this.benchmark.record('divergenceComputation', start);\n return null;\n }\n\n const real = MarkovGraph.logLikelihoodTrajectory(\n this.graph,\n trajectory,\n this.trajectorySmoothingEpsilon,\n );\n const expected = MarkovGraph.logLikelihoodTrajectory(\n this.baseline,\n trajectory,\n this.trajectorySmoothingEpsilon,\n );\n\n const N = Math.max(1, trajectory.length - 1);\n const expectedAvg = expected / N;\n const threshold = -Math.abs(this.graph.divergenceThreshold);\n\n const hasCalibratedBaseline =\n typeof this.graph.baselineMeanLL === 'number' &&\n typeof this.graph.baselineStdLL === 'number' &&\n Number.isFinite(this.graph.baselineMeanLL) &&\n Number.isFinite(this.graph.baselineStdLL) &&\n this.graph.baselineStdLL > 0;\n\n const adjustedStd = hasCalibratedBaseline\n ? this.graph.baselineStdLL * Math.sqrt(MAX_WINDOW_LENGTH / N)\n : 0;\n\n const zScore = hasCalibratedBaseline\n ? (expectedAvg - this.graph.baselineMeanLL) / adjustedStd\n : expectedAvg;\n\n const shouldEmit = hasCalibratedBaseline ? zScore <= threshold : expectedAvg <= threshold;\n\n this.benchmark.record('divergenceComputation', start);\n\n if (shouldEmit) {\n const sampleSize = this.graph.rowTotal(from);\n return {\n kind: 'trajectory_anomaly',\n payload: {\n stateFrom: from,\n stateTo: to,\n realLogLikelihood: real,\n expectedBaselineLogLikelihood: expected,\n zScore,\n sampleSize,\n confidence: getConfidence(sampleSize),\n },\n };\n }\n\n return null;\n }\n\n /* ================================================================== */\n /* Dwell-Time Anomaly Detection */\n /* ================================================================== */\n\n /**\n * Evaluate dwell time on the *previous* state via Welford's online algorithm\n * and return a `DwellDecision` when the z-score exceeds the configured\n * threshold, or `null` otherwise.\n *\n * The Welford accumulator is always updated regardless of whether a decision\n * is produced — this ensures the running mean/std improves with every sample.\n *\n * This method is a **pure evaluator** with one intentional statistical\n * side-effect: the per-state `dwellStats` accumulator is mutated so that\n * successive calls converge on accurate mean and standard-deviation estimates.\n * No events are emitted here.\n */\n evaluateDwellTime(state: string, dwellMs: number): DwellDecision | null {\n // Gating (dwellTimeEnabled) is handled by DwellTimePolicy; this method\n // is only called when the policy exists and has decided dwell should be\n // evaluated for this transition.\n if (dwellMs <= 0) return null;\n\n const updated = updateDwellStats(this.dwellStats.get(state), dwellMs);\n this.dwellStats.set(state, updated);\n\n if (updated.count < this.dwellTimeMinSamples) return null;\n\n const std = dwellStd(updated);\n if (std <= 0) return null;\n\n const zScore = (dwellMs - updated.meanMs) / std;\n\n if (Math.abs(zScore) >= this.dwellTimeZScoreThreshold) {\n const sampleSize = updated.count;\n return {\n kind: 'dwell_time_anomaly',\n payload: {\n state,\n dwellMs,\n meanMs: updated.meanMs,\n stdMs: std,\n zScore,\n sampleSize,\n confidence: getConfidence(sampleSize),\n },\n isPositiveZScore: zScore > 0,\n };\n }\n\n return null;\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport type {\n AsyncStorageAdapter,\n StorageAdapter,\n TimerAdapter,\n TimerHandle,\n} from '../adapters.js';\nimport type { MarkovGraph, SerializedMarkovGraph } from '../core/markov.js';\nimport type { BloomFilter } from '../core/bloom.js';\nimport { uint8ToBase64 } from '../persistence/codec.js';\nimport type { PassiveIntentError, PassiveIntentTelemetry } from '../types/events.js';\n\nexport interface PersistedPayload {\n bloomBase64: string;\n graphBinary?: string;\n graph?: SerializedMarkovGraph;\n}\n\nexport interface PersistStrategyContext {\n getStorageKey(): string;\n getStorage(): StorageAdapter;\n getAsyncStorage(): AsyncStorageAdapter | null;\n getTimer(): TimerAdapter;\n getThrottleMs(): number;\n getDebounceMs(): number;\n getGraphAndBloom(): { graph: MarkovGraph; bloom: BloomFilter } | null;\n\n isClosed(): boolean;\n isDirty(): boolean;\n clearDirty(): void;\n markDirty(): void;\n\n setEngineHealth(health: PassiveIntentTelemetry['engineHealth']): void;\n reportError(code: PassiveIntentError['code'], message: string, err: unknown): void;\n}\n\nexport interface PersistStrategy {\n persist(): void;\n flushNow(): void;\n close(): void;\n}\n\nexport abstract class BasePersistStrategy implements PersistStrategy {\n protected lastPersistedAt = -Infinity;\n protected throttleTimer: TimerHandle | null = null;\n protected isClosedFlag = false;\n\n constructor(protected readonly ctx: PersistStrategyContext) {}\n\n abstract persist(): void;\n\n protected serialize(): string | null {\n const data = this.ctx.getGraphAndBloom();\n if (!data) return null;\n const { graph, bloom } = data;\n\n this.ctx.setEngineHealth('pruning_active');\n try {\n graph.prune();\n } finally {\n this.ctx.setEngineHealth('healthy');\n }\n\n let graphBinary: string;\n try {\n const graphBytes = graph.toBinary();\n graphBinary = uint8ToBase64(graphBytes);\n } catch (err) {\n this.ctx.reportError('SERIALIZE', err instanceof Error ? err.message : String(err), err);\n return null;\n }\n\n return JSON.stringify({\n bloomBase64: bloom.toBase64(),\n graphBinary,\n });\n }\n\n flushNow(): void {\n if (this.throttleTimer !== null) {\n this.ctx.getTimer().clearTimeout(this.throttleTimer);\n this.throttleTimer = null;\n }\n this.lastPersistedAt = -Infinity;\n this.persist();\n }\n\n close(): void {\n this.isClosedFlag = true;\n if (this.throttleTimer !== null) {\n this.ctx.getTimer().clearTimeout(this.throttleTimer);\n this.throttleTimer = null;\n }\n }\n\n protected checkThrottle(): boolean {\n const throttleMs = this.ctx.getThrottleMs();\n if (throttleMs > 0 && !this.isClosedFlag) {\n const now = this.ctx.getTimer().now();\n const elapsed = now - this.lastPersistedAt;\n if (elapsed < throttleMs) {\n if (this.throttleTimer === null) {\n const remainingMs = throttleMs - elapsed;\n this.throttleTimer = this.ctx.getTimer().setTimeout(() => {\n this.throttleTimer = null;\n this.persist();\n }, remainingMs);\n }\n return true;\n }\n }\n return false;\n }\n}\n\nexport class SyncPersistStrategy extends BasePersistStrategy {\n persist(): void {\n if (!this.ctx.isDirty() || !this.ctx.getGraphAndBloom()) return;\n\n if (this.checkThrottle()) return;\n\n const payload = this.serialize();\n if (!payload) return;\n\n try {\n this.ctx.getStorage().setItem(this.ctx.getStorageKey(), payload);\n this.ctx.clearDirty();\n this.lastPersistedAt = this.ctx.getTimer().now();\n this.ctx.setEngineHealth('healthy');\n } catch (err) {\n const isQuota =\n err instanceof Error &&\n (err.name === 'QuotaExceededError' || err.message.toLowerCase().includes('quota'));\n if (isQuota) {\n this.ctx.setEngineHealth('quota_exceeded');\n }\n this.ctx.reportError(\n isQuota ? 'QUOTA_EXCEEDED' : 'STORAGE_WRITE',\n err instanceof Error ? err.message : String(err),\n err,\n );\n }\n }\n}\n\nexport class AsyncPersistStrategy extends BasePersistStrategy {\n private isAsyncWriting = false;\n private hasPendingAsyncPersist = false;\n private asyncWriteFailCount = 0;\n private retryTimer: TimerHandle | null = null;\n\n persist(): void {\n if (!this.ctx.isDirty() || !this.ctx.getGraphAndBloom()) return;\n\n if (this.isAsyncWriting) {\n this.hasPendingAsyncPersist = true;\n return;\n }\n\n if (this.checkThrottle()) return;\n\n const payload = this.serialize();\n if (!payload) return;\n\n this.isAsyncWriting = true;\n this.hasPendingAsyncPersist = false;\n this.ctx.clearDirty();\n\n const asyncStorage = this.ctx.getAsyncStorage()!;\n let setItemPromise: Promise<void>;\n try {\n setItemPromise = asyncStorage.setItem(this.ctx.getStorageKey(), payload);\n } catch (err: unknown) {\n this.handleAsyncWriteError(err);\n return;\n }\n\n setItemPromise\n .then(() => {\n this.isAsyncWriting = false;\n this.asyncWriteFailCount = 0;\n this.lastPersistedAt = this.ctx.getTimer().now();\n this.ctx.setEngineHealth('healthy');\n if (this.hasPendingAsyncPersist || this.ctx.isDirty()) {\n this.hasPendingAsyncPersist = false;\n this.persist();\n }\n })\n .catch((err: unknown) => {\n this.handleAsyncWriteError(err);\n });\n }\n\n private handleAsyncWriteError(err: unknown): void {\n this.isAsyncWriting = false;\n this.ctx.markDirty();\n this.asyncWriteFailCount += 1;\n\n const isQuota =\n err instanceof Error &&\n (err.name === 'QuotaExceededError' || err.message.toLowerCase().includes('quota'));\n if (isQuota) {\n this.ctx.setEngineHealth('quota_exceeded');\n }\n this.ctx.reportError(\n isQuota ? 'QUOTA_EXCEEDED' : 'STORAGE_WRITE',\n err instanceof Error ? err.message : String(err),\n err,\n );\n\n this.hasPendingAsyncPersist = false;\n if (!this.isClosedFlag && this.asyncWriteFailCount === 1) {\n this.schedulePersist();\n }\n }\n\n private schedulePersist(): void {\n if (this.isClosedFlag) return;\n if (this.retryTimer !== null) {\n this.ctx.getTimer().clearTimeout(this.retryTimer);\n }\n this.retryTimer = this.ctx.getTimer().setTimeout(() => {\n this.retryTimer = null;\n this.persist();\n }, this.ctx.getDebounceMs());\n }\n\n override flushNow(): void {\n if (this.retryTimer !== null) {\n this.ctx.getTimer().clearTimeout(this.retryTimer);\n this.retryTimer = null;\n }\n super.flushNow();\n }\n\n override close(): void {\n super.close();\n if (this.retryTimer !== null) {\n this.ctx.getTimer().clearTimeout(this.retryTimer);\n this.retryTimer = null;\n }\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport type { AsyncStorageAdapter, StorageAdapter, TimerAdapter } from '../adapters.js';\nimport type { MarkovGraph } from '../core/markov.js';\nimport type { BloomFilter } from '../core/bloom.js';\nimport { BloomFilter as BloomFilterClass } from '../core/bloom.js';\nimport { MarkovGraph as MarkovGraphClass } from '../core/markov.js';\nimport { base64ToUint8 } from '../persistence/codec.js';\nimport type {\n MarkovGraphConfig,\n PassiveIntentError,\n PassiveIntentTelemetry,\n} from '../types/events.js';\n\nimport { SyncPersistStrategy, AsyncPersistStrategy } from './persistence-strategies.js';\nimport type {\n PersistStrategy,\n PersistStrategyContext,\n PersistedPayload,\n} from './persistence-strategies.js';\n\n/**\n * Configuration for PersistenceCoordinator.\n * All values are resolved and defaulted by IntentManager before being passed in.\n */\nexport interface PersistenceCoordinatorConfig {\n storageKey: string;\n persistDebounceMs: number;\n persistThrottleMs: number;\n storage: StorageAdapter;\n asyncStorage: AsyncStorageAdapter | null;\n timer: TimerAdapter;\n onError?: (err: PassiveIntentError) => void;\n}\n\n/**\n * PersistenceCoordinator — owns all write/read/retry orchestration.\n *\n * Responsibilities:\n * - `restore()` — deserialise Bloom filter + Markov graph from storage on startup\n * - `persist()` — prune → serialize → write (sync or async path)\n * - Dirty-flag short-circuit (no-op when nothing changed)\n * - Throttle gate with trailing-flush timer\n * - Async write guard (prevents overlapping `setItem` calls)\n * - One-shot retry on first consecutive async write failure\n * - `engineHealth` status flag exposed to telemetry\n * - `flushNow()` — bypasses throttle for teardown/forced flush\n *\n * Call `attach(graph, bloom)` once after restoring/constructing the Markov graph\n * and Bloom filter so the coordinator can serialise them on each `persist()`.\n */\nexport class PersistenceCoordinator implements PersistStrategyContext {\n private readonly storageKey: string;\n private readonly persistDebounceMs: number;\n private readonly persistThrottleMs: number;\n private readonly storage: StorageAdapter;\n private readonly asyncStorage: AsyncStorageAdapter | null;\n private readonly timer: TimerAdapter;\n private readonly strategy: PersistStrategy;\n private readonly onErrorCb?: (err: PassiveIntentError) => void;\n\n /* Late-bound after attach() */\n private graphInstance: MarkovGraph | null = null;\n private bloomInstance: BloomFilter | null = null;\n\n /* Lifecycle */\n private isClosedFlag = false;\n\n /* Mutable coordination flags — internal only */\n private isDirtyFlag = false;\n private engineHealthInternal: PassiveIntentTelemetry['engineHealth'] = 'healthy';\n\n /** Signal that new state has been written and needs to be persisted. */\n markDirty(): void {\n this.isDirtyFlag = true;\n }\n\n constructor(config: PersistenceCoordinatorConfig) {\n this.storageKey = config.storageKey;\n this.persistDebounceMs = config.persistDebounceMs;\n this.persistThrottleMs = config.persistThrottleMs;\n this.storage = config.storage;\n this.asyncStorage = config.asyncStorage;\n this.timer = config.timer;\n this.onErrorCb = config.onError;\n\n if (this.asyncStorage) {\n this.strategy = new AsyncPersistStrategy(this);\n } else {\n this.strategy = new SyncPersistStrategy(this);\n }\n }\n\n // --- PersistStrategyContext implementation ---\n\n getStorageKey() {\n return this.storageKey;\n }\n getStorage() {\n return this.storage;\n }\n getAsyncStorage() {\n return this.asyncStorage;\n }\n getTimer() {\n return this.timer;\n }\n getThrottleMs() {\n return this.persistThrottleMs;\n }\n getDebounceMs() {\n return this.persistDebounceMs;\n }\n getGraphAndBloom() {\n if (!this.graphInstance || !this.bloomInstance) return null;\n return { graph: this.graphInstance, bloom: this.bloomInstance };\n }\n isClosed() {\n return this.isClosedFlag;\n }\n isDirty() {\n return this.isDirtyFlag;\n }\n clearDirty() {\n this.isDirtyFlag = false;\n }\n setEngineHealth(health: PassiveIntentTelemetry['engineHealth']) {\n this.engineHealthInternal = health;\n }\n reportError(code: PassiveIntentError['code'], message: string, err: unknown) {\n if (this.onErrorCb) {\n this.onErrorCb({ code, message, originalError: err });\n }\n }\n\n get engineHealth(): PassiveIntentTelemetry['engineHealth'] {\n return this.engineHealthInternal;\n }\n\n /**\n * Provide the live graph and bloom filter after they have been constructed.\n * Must be called before the first `persist()`.\n */\n attach(graph: MarkovGraph, bloom: BloomFilter): void {\n this.graphInstance = graph;\n this.bloomInstance = bloom;\n }\n\n /* ================================================================== */\n /* Restore */\n /* ================================================================== */\n\n /**\n * Attempt to read and deserialise a previously persisted payload.\n * Returns `null` on first run (no stored data) or if the data is corrupt.\n * Never throws — all errors go to `onError`.\n */\n restore(graphConfig: MarkovGraphConfig): { bloom: BloomFilter; graph: MarkovGraph } | null {\n let raw: string | null;\n try {\n raw = this.storage.getItem(this.storageKey);\n } catch (err) {\n if (this.onErrorCb) {\n this.onErrorCb({\n code: 'STORAGE_READ',\n message: err instanceof Error ? err.message : String(err),\n originalError: err,\n });\n }\n return null;\n }\n\n if (!raw) return null;\n\n try {\n const parsed = JSON.parse(raw) as PersistedPayload;\n\n let graph: MarkovGraph;\n if (parsed.graphBinary) {\n const bytes = base64ToUint8(parsed.graphBinary);\n graph = MarkovGraphClass.fromBinary(bytes, graphConfig);\n } else if (parsed.graph) {\n // Legacy JSON format — predates the binary codec.\n graph = MarkovGraphClass.fromJSON(parsed.graph, graphConfig);\n } else {\n return null;\n }\n\n const bloom = parsed.bloomBase64\n ? BloomFilterClass.fromBase64(parsed.bloomBase64)\n : new BloomFilterClass();\n\n return { bloom, graph };\n } catch (err) {\n if (this.onErrorCb) {\n const payloadByteLength =\n typeof TextEncoder !== 'undefined'\n ? new TextEncoder().encode(raw as string).length\n : (raw as string).length;\n this.onErrorCb({\n code: 'RESTORE_PARSE',\n message: err instanceof Error ? err.message : String(err),\n originalError: { cause: err, payloadLength: payloadByteLength },\n });\n }\n return null;\n }\n }\n\n persist(): void {\n this.strategy.persist();\n }\n flushNow(): void {\n this.strategy.flushNow();\n }\n close(): void {\n this.isClosedFlag = true;\n this.strategy.close();\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport { BrowserLifecycleAdapter } from '../adapters.js';\nimport type { LifecycleAdapter, TimerAdapter, TimerHandle } from '../adapters.js';\nimport { EventEmitter } from './event-emitter.js';\nimport type { IntentEventMap } from '../types/events.js';\nimport {\n ATTENTION_RETURN_THRESHOLD_MS,\n MAX_PLAUSIBLE_DWELL_MS,\n USER_IDLE_THRESHOLD_MS,\n} from './constants.js';\n\n/**\n * Configuration for LifecycleCoordinator.\n *\n * All callbacks are supplied by IntentManager and run synchronously\n * inside the onResume handler — no closure captures of coordinator internals.\n */\nexport interface LifecycleCoordinatorConfig {\n /**\n * Optional pre-built adapter. When `undefined` (not set), a new\n * `BrowserLifecycleAdapter` is created internally (and owned). Explicitly\n * passing `null` disables lifecycle tracking entirely.\n *\n * The `undefined` vs. `null` distinction is intentional and mirrors the\n * original `IntentManager` constructor behaviour.\n */\n lifecycleAdapter?: LifecycleAdapter | null;\n timer: TimerAdapter;\n dwellTimeEnabled: boolean;\n emitter: EventEmitter<IntentEventMap>;\n /**\n * Called on resume when the hidden duration was within the plausible range.\n * IntentManager should execute: `previousStateEnteredAt += delta`.\n */\n onAdjustBaseline: (delta: number) => void;\n /**\n * Called on resume when the hidden duration exceeded `MAX_PLAUSIBLE_DWELL_MS`.\n * IntentManager should execute: `previousStateEnteredAt = timer.now()`.\n */\n onResetBaseline: () => void;\n /**\n * Returns whether the engine has an active dwell epoch in progress.\n * Translates to `previousState !== null` in IntentManager.\n */\n hasPreviousState: () => boolean;\n /**\n * Returns the current state the user is viewing, or `null` when no state\n * has been entered yet. Used by the comparison-shopper detection path.\n */\n getPreviousState: () => string | null;\n /**\n * Called when exit intent is detected and Markov math confirms a\n * likely continuation path. Supplied by `IntentManager` which performs\n * the probability check and emits the `exit_intent` event.\n *\n * Optional — when absent, exit-intent detection is disabled even if the\n * underlying `LifecycleAdapter` supports `onExitIntent`.\n */\n onExitIntent?: () => void;\n}\n\n/**\n * LifecycleCoordinator — owns pause/resume handling and session-stale detection.\n *\n * Responsibilities:\n * - Creates or accepts an injected `LifecycleAdapter`\n * - On tab hide (pause): records `tabHiddenAt`\n * - On tab show (resume):\n * - Computes hidden duration\n * - When within plausible range: calls `onAdjustBaseline(duration)` to\n * offset `previousStateEnteredAt` so the dwell clock ignores off-screen time\n * - When duration exceeds `MAX_PLAUSIBLE_DWELL_MS` (OS suspend / hibernate):\n * calls `onResetBaseline()` and emits `session_stale`\n * - Both paths are skipped when `dwellTimeEnabled` is `false`\n * - `destroy()`: deregisters callbacks and (conditionally) tears down the adapter\n * - Idle detection (when the adapter supports `onInteraction`):\n * - Resets a `USER_IDLE_THRESHOLD_MS` debounce timer on every interaction\n * - Emits `user_idle` when the timer fires (no interaction for that duration)\n * - Emits `user_resumed` on the first interaction after an idle period\n * - Adjusts the dwell baseline to exclude idle duration\n */\nexport class LifecycleCoordinator {\n private readonly timer: TimerAdapter;\n private readonly dwellTimeEnabled: boolean;\n private readonly emitter: EventEmitter<IntentEventMap>;\n private readonly onAdjustBaseline: (delta: number) => void;\n private readonly onResetBaseline: () => void;\n private readonly hasPreviousState: () => boolean;\n private readonly getPreviousState: () => string | null;\n\n private lifecycleAdapter: LifecycleAdapter | null;\n private ownsLifecycleAdapter: boolean;\n private pauseUnsub: (() => void) | null = null;\n private resumeUnsub: (() => void) | null = null;\n private exitIntentUnsub: (() => void) | null = null;\n\n /** Timestamp when the tab last became hidden; `null` while visible. */\n private tabHiddenAt: number | null = null;\n\n /* ── Idle-state detection ───────────────────────────────────────────── */\n private lastInteractionAt: number;\n private idleStartedAt: number = 0;\n private isIdle: boolean = false;\n private idleCheckTimer: TimerHandle | null = null;\n private interactionUnsub: (() => void) | null = null;\n\n private readonly onExitIntentCallback: (() => void) | undefined;\n\n constructor(config: LifecycleCoordinatorConfig) {\n this.timer = config.timer;\n this.dwellTimeEnabled = config.dwellTimeEnabled;\n this.emitter = config.emitter;\n this.onAdjustBaseline = config.onAdjustBaseline;\n this.onResetBaseline = config.onResetBaseline;\n this.hasPreviousState = config.hasPreviousState;\n this.getPreviousState = config.getPreviousState;\n this.lastInteractionAt = this.timer.now();\n\n this.onExitIntentCallback = config.onExitIntent;\n\n // Preserve the exact undefined-vs-null semantics from the original constructor:\n // - undefined → create a BrowserLifecycleAdapter (owned by this coordinator)\n // - null → no adapter; lifecycle tracking disabled\n // - object → injected adapter; this coordinator does NOT own it\n if (config.lifecycleAdapter !== undefined) {\n this.lifecycleAdapter = config.lifecycleAdapter;\n this.ownsLifecycleAdapter = false;\n } else {\n this.lifecycleAdapter = typeof window !== 'undefined' ? new BrowserLifecycleAdapter() : null;\n this.ownsLifecycleAdapter = true;\n }\n\n this.bindAdapter(this.lifecycleAdapter);\n }\n\n /**\n * Unsubscribe any existing pause/resume callbacks and re-register them on\n * `adapter` (or leave them null when `adapter` is null).\n *\n * Single source of truth for handler registration — used by both the\n * constructor and `setAdapterForTest`.\n */\n private bindAdapter(adapter: LifecycleAdapter | null): void {\n this.pauseUnsub?.();\n this.pauseUnsub = null;\n this.resumeUnsub?.();\n this.resumeUnsub = null;\n this.exitIntentUnsub?.();\n this.exitIntentUnsub = null;\n this.stopIdleTracking();\n\n if (!adapter) return;\n\n this.pauseUnsub = adapter.onPause(() => {\n this.tabHiddenAt = this.timer.now();\n });\n\n this.resumeUnsub = adapter.onResume(() => {\n if (this.tabHiddenAt === null) return;\n const hiddenDuration = this.timer.now() - this.tabHiddenAt;\n this.tabHiddenAt = null;\n\n // ── Comparison-shopper detection (attention_return) ─────────────────\n // When the user returns after ≥15 s and was viewing a known state,\n // emit an attention_return event so the host app can surface a\n // \"Welcome Back\" experience (e.g. discount modal).\n if (hiddenDuration >= ATTENTION_RETURN_THRESHOLD_MS) {\n const currentState = this.getPreviousState();\n if (currentState !== null) {\n this.emitter.emit('attention_return', {\n state: currentState,\n hiddenDuration,\n });\n }\n }\n\n // All resume-path operations are dwell-time bookkeeping. When dwell-time\n // detection is disabled the caller has opted out of all dwell diagnostics.\n if (!this.dwellTimeEnabled) return;\n\n if (hiddenDuration > MAX_PLAUSIBLE_DWELL_MS) {\n // Almost certainly an OS suspend / hibernate event. Only act when an\n // active dwell epoch is in progress.\n if (this.hasPreviousState()) {\n this.onResetBaseline();\n this.emitter.emit('session_stale', {\n reason: 'hidden_duration_exceeded',\n measuredMs: hiddenDuration,\n thresholdMs: MAX_PLAUSIBLE_DWELL_MS,\n });\n }\n return;\n }\n\n // Offset the dwell baseline only when a state has been entered.\n if (this.hasPreviousState()) {\n this.onAdjustBaseline(hiddenDuration);\n }\n });\n\n this.startIdleTracking(adapter);\n\n // Subscribe to exit-intent detection when the adapter supports it and the\n // coordinator has been given a callback to invoke.\n if (this.onExitIntentCallback !== undefined && typeof adapter.onExitIntent === 'function') {\n this.exitIntentUnsub = adapter.onExitIntent(() => {\n this.onExitIntentCallback!();\n });\n }\n }\n\n /**\n * @internal — test-only hook. Replaces the active lifecycle adapter after\n * construction: unsubscribes the coordinator's pause/resume callbacks from\n * the previous adapter, swaps in the new one, and re-registers the same\n * handlers on the new adapter (or leaves them null when `adapter` is null).\n *\n * Do NOT call this in production code; injecting the adapter via\n * `LifecycleCoordinatorConfig.lifecycleAdapter` is the correct approach.\n */\n /* @internal */\n setAdapterForTest(adapter: LifecycleAdapter | null, owns: boolean): void {\n if (this.ownsLifecycleAdapter) {\n this.lifecycleAdapter?.destroy();\n }\n this.lifecycleAdapter = adapter;\n this.ownsLifecycleAdapter = owns;\n this.tabHiddenAt = null;\n this.isIdle = false;\n this.lastInteractionAt = this.timer.now();\n this.exitIntentUnsub?.();\n this.exitIntentUnsub = null;\n this.bindAdapter(adapter);\n }\n\n /**\n * Deregister this instance's specific pause/resume callbacks and, if this\n * coordinator owns its adapter, destroy it.\n *\n * Injected (shared) adapters are NOT destroyed here — they may serve other\n * `IntentManager` instances and must be torn down by their owner.\n */\n destroy(): void {\n this.stopIdleTracking();\n this.pauseUnsub?.();\n this.pauseUnsub = null;\n this.resumeUnsub?.();\n this.resumeUnsub = null;\n this.exitIntentUnsub?.();\n this.exitIntentUnsub = null;\n if (this.ownsLifecycleAdapter) {\n this.lifecycleAdapter?.destroy();\n }\n }\n\n /* ── Idle-state detection internals ────────────────────────────────── */\n\n /**\n * Set up the interaction subscription and debounce-based idle detection.\n * Gracefully skips when the adapter does not implement `onInteraction`.\n *\n * Every interaction resets a `USER_IDLE_THRESHOLD_MS` one-shot timer.\n * When the timer fires without being cancelled, the user is considered idle.\n */\n private startIdleTracking(adapter: LifecycleAdapter | null): void {\n if (!adapter || typeof adapter.onInteraction !== 'function') return;\n\n const armIdleTimer = (): void => {\n if (this.idleCheckTimer !== null) {\n this.timer.clearTimeout(this.idleCheckTimer);\n }\n this.idleCheckTimer = this.timer.setTimeout(() => {\n this.idleCheckTimer = null;\n if (this.isIdle || !this.hasPreviousState()) return;\n\n this.isIdle = true;\n this.idleStartedAt = this.lastInteractionAt + USER_IDLE_THRESHOLD_MS;\n\n const currentState = this.getPreviousState();\n if (currentState !== null) {\n this.emitter.emit('user_idle', {\n state: currentState,\n idleMs: this.timer.now() - this.idleStartedAt,\n });\n }\n }, USER_IDLE_THRESHOLD_MS);\n };\n\n const unsub = adapter.onInteraction(() => {\n this.lastInteractionAt = this.timer.now();\n\n if (this.isIdle) {\n const idleMs = this.timer.now() - this.idleStartedAt;\n this.isIdle = false;\n\n // Exclude the idle period from the dwell clock.\n if (this.dwellTimeEnabled && this.hasPreviousState()) {\n this.onAdjustBaseline(idleMs);\n }\n\n const currentState = this.getPreviousState();\n if (currentState !== null) {\n this.emitter.emit('user_resumed', {\n state: currentState,\n idleMs,\n });\n }\n }\n\n armIdleTimer();\n });\n\n // The adapter returned null — it cannot deliver interaction events\n // (e.g. SSR / Node.js with a stubbed window). Skip idle tracking.\n if (!unsub) return;\n\n this.interactionUnsub = unsub;\n // Arm the initial timer so idle is detected even with no interactions.\n armIdleTimer();\n }\n\n /** Tear down the interaction listener and idle-check timer. */\n private stopIdleTracking(): void {\n this.interactionUnsub?.();\n this.interactionUnsub = null;\n if (this.idleCheckTimer !== null) {\n this.timer.clearTimeout(this.idleCheckTimer);\n this.idleCheckTimer = null;\n }\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport type { EnginePolicy, PolicyTrackContext } from './engine-policy.js';\nimport type { EventEmitter } from '../event-emitter.js';\nimport type { IntentEventMap } from '../../types/events.js';\nimport { MAX_PLAUSIBLE_DWELL_MS } from '../constants.js';\n\n/**\n * Dependencies injected by IntentManager at construction time.\n */\nexport interface DwellTimePolicyConfig {\n /** Returns `true` when the EntropyGuard flags the session as a bot. */\n isSuspected: () => boolean;\n /** Delegate — forwards the measured dwell to SignalEngine's Welford accumulator. */\n evaluateDwellTime: (state: string, dwellMs: number) => void;\n /** Returns the timestamp when the previous state was entered. */\n getPreviousStateEnteredAt: () => number;\n /** Shared event emitter for `session_stale` emission. */\n emitter: EventEmitter<IntentEventMap>;\n}\n\n/**\n * DwellTimePolicy — gates and measures dwell-time on each state transition.\n *\n * Replaces the inline `if (this.dwellTimeEnabled && …)` conditional that was\n * previously in `IntentManager.runTransitionContextStage`. When this policy\n * is **not** instantiated (because `dwellTime.enabled` is `false`), no\n * dwell-time logic executes at all.\n *\n * Responsibilities:\n * - Measure elapsed time since the previous state was entered.\n * - Guard against implausibly large dwell durations caused by OS suspend\n * (emits `session_stale` with `reason: 'dwell_exceeded'`).\n * - Delegate valid measurements to `SignalEngine.evaluateDwellTime()`.\n * - Skip evaluation when the session is flagged as a suspected bot.\n */\nexport class DwellTimePolicy implements EnginePolicy {\n private readonly isSuspected: () => boolean;\n private readonly evaluateDwellTime: (state: string, dwellMs: number) => void;\n private readonly getPreviousStateEnteredAt: () => number;\n private readonly emitter: EventEmitter<IntentEventMap>;\n\n constructor(config: DwellTimePolicyConfig) {\n this.isSuspected = config.isSuspected;\n this.evaluateDwellTime = config.evaluateDwellTime;\n this.getPreviousStateEnteredAt = config.getPreviousStateEnteredAt;\n this.emitter = config.emitter;\n }\n\n onTrackContext(ctx: PolicyTrackContext): void {\n if (!ctx.from || this.isSuspected()) return;\n\n const dwellMs = ctx.now - this.getPreviousStateEnteredAt();\n\n if (dwellMs > MAX_PLAUSIBLE_DWELL_MS) {\n // Implausibly large dwell — CPU suspend or OS hibernation slipped past\n // the LifecycleAdapter. Discard the measurement and emit a diagnostic\n // event.\n //\n // CONTRACT: IntentManager.runTransitionContextStage unconditionally sets\n // `previousStateEnteredAt = ctx.now` immediately after all onTrackContext\n // hooks complete (including this one), so the stale baseline is always\n // cleared regardless of which branch executes here. If that ordering\n // ever changes, this branch must perform the reset explicitly.\n this.emitter.emit('session_stale', {\n reason: 'dwell_exceeded',\n measuredMs: dwellMs,\n thresholdMs: MAX_PLAUSIBLE_DWELL_MS,\n });\n } else {\n this.evaluateDwellTime(ctx.from, dwellMs);\n }\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport type { EnginePolicy } from './engine-policy.js';\nimport type { MarkovGraph } from '../../core/markov.js';\n\n/**\n * BigramPolicy — records second-order (bigram) Markov transitions.\n *\n * Replaces the inline `if (this.enableBigrams && trajectory.length >= 3)`\n * conditional that was previously in `SignalEngine.recordTransition`.\n * When this policy is **not** instantiated (because `enableBigrams` is\n * `false`), no bigram accounting executes at all.\n *\n * Bigram states are encoded as `\"prev→from\"` → `\"from→to\"` using U+2192\n * as a collision-resistant separator that will not appear in normal URL-based\n * state labels.\n *\n * The frequency-threshold guard prevents sparse bigram pollution during the\n * early learning phase: bigrams are only recorded when the *unigram* source\n * state (`from`) has accumulated at least `bigramFrequencyThreshold`\n * outgoing transitions.\n */\nexport class BigramPolicy implements EnginePolicy {\n private readonly graph: MarkovGraph;\n private readonly bigramFrequencyThreshold: number;\n\n constructor(graph: MarkovGraph, bigramFrequencyThreshold: number) {\n this.graph = graph;\n this.bigramFrequencyThreshold = bigramFrequencyThreshold;\n }\n\n onTransition(from: string, to: string, trajectory: readonly string[]): void {\n if (trajectory.length < 3) return;\n\n const prev2 = trajectory[trajectory.length - 3];\n const bigramFrom = `${prev2}\\u2192${from}`;\n const bigramTo = `${from}\\u2192${to}`;\n\n // Only record when the unigram source has enough outgoing transitions.\n if (this.graph.rowTotal(from) >= this.bigramFrequencyThreshold) {\n this.graph.incrementTransition(bigramFrom, bigramTo);\n }\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport type { EnginePolicy } from './engine-policy.js';\nimport type { PassiveIntentTelemetry } from '../../types/events.js';\n\n/**\n * DriftProtectionPolicy — failsafe killswitch that silences trajectory\n * evaluation when the anomaly rate exceeds a configurable threshold.\n *\n * Replaces the following state that was previously scattered across\n * `SignalEngine`:\n * - `isBaselineDriftedInternal` flag\n * - `driftWindowStart`, `driftWindowTrackCount`, `driftWindowAnomalyCount`\n * - `advanceDriftWindow(now)` method\n * - Drift anomaly counting inside `evaluateTrajectory()`\n * - `baselineStatus` / `isBaselineDrifted` getters\n *\n * Lifecycle:\n * 1. `onTrackStart(now)` is called once per `track()` to advance the\n * rolling evaluation window.\n * 2. `SignalEngine.evaluateTrajectory()` checks `isDrifted` to decide\n * whether to skip evaluation, and calls `recordAnomaly()` when a\n * trajectory anomaly is detected.\n */\nexport class DriftProtectionPolicy implements EnginePolicy {\n private readonly maxAnomalyRate: number;\n private readonly evaluationWindowMs: number;\n\n private drifted = false;\n private windowStart = 0;\n private windowTrackCount = 0;\n private windowAnomalyCount = 0;\n\n constructor(maxAnomalyRate: number, evaluationWindowMs: number) {\n this.maxAnomalyRate = maxAnomalyRate;\n this.evaluationWindowMs = evaluationWindowMs;\n }\n\n /* ── EnginePolicy hook ──────────────────────────────────────────────── */\n\n /**\n * Advance the rolling evaluation window. Resets counters when the window\n * elapses. O(1), no allocations.\n */\n onTrackStart(now: number): void {\n if (now - this.windowStart >= this.evaluationWindowMs) {\n this.windowStart = now;\n this.windowTrackCount = 0;\n this.windowAnomalyCount = 0;\n }\n this.windowTrackCount += 1;\n }\n\n /* ── Queried by SignalEngine ────────────────────────────────────────── */\n\n /** `true` once the anomaly rate has breached the threshold in any window. */\n get isDrifted(): boolean {\n return this.drifted;\n }\n\n /** Telemetry-friendly status label. */\n get baselineStatus(): PassiveIntentTelemetry['baselineStatus'] {\n return this.drifted ? 'drifted' : 'active';\n }\n\n /**\n * Called by `SignalEngine.evaluateTrajectory()` each time a trajectory\n * anomaly is detected (regardless of cooldown gating). Increments the\n * window anomaly counter and flips the `drifted` flag when the rate\n * exceeds `maxAnomalyRate`.\n */\n recordAnomaly(): void {\n this.windowAnomalyCount += 1;\n if (\n this.windowTrackCount > 0 &&\n this.windowAnomalyCount / this.windowTrackCount > this.maxAnomalyRate\n ) {\n this.drifted = true;\n }\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport type { BloomFilter } from '../core/bloom.js';\nimport type { MarkovGraph } from '../core/markov.js';\n\n/**\n * Hard upper bound on each state label accepted over the broadcast channel.\n *\n * A compromised tab could send arbitrarily long strings to exhaust memory in\n * every other tab (heap amplification / buffer-overflow equivalent in JS).\n * Payloads that exceed this limit are **silently dropped** so that a single\n * bad tab cannot degrade the rest of the session fleet.\n */\nexport const MAX_STATE_LENGTH = 256;\n\n/**\n * Wire format of a Markov-transition sync message.\n */\ninterface TransitionMessage {\n /** Discriminant. */\n type: 'transition';\n /** The state the user navigated *from*. */\n from: string;\n /** The state the user navigated *to*. */\n to: string;\n}\n\n/**\n * Wire format of a deterministic counter increment sync message.\n *\n * Only positive, finite `by` values are acted on; malformed or unbounded\n * payloads are silently dropped.\n */\ninterface CounterMessage {\n /** Discriminant. */\n type: 'counter';\n /** Counter key — same length constraints as state labels (≤ MAX_STATE_LENGTH). */\n key: string;\n /** Amount to add to the counter. Must be finite; validated before use. */\n by: number;\n}\n\n/** Union of all wire-protocol message types. */\ntype SyncMessage = TransitionMessage | CounterMessage;\n\n/**\n * Type-guard for `TransitionMessage`.\n *\n * Rejects the payload if `from` or `to` exceed `MAX_STATE_LENGTH` (memory-\n * exhaustion / model-poisoning guard).\n */\nfunction isValidTransitionMessage(msg: Record<string, unknown>): boolean {\n if (msg['type'] !== 'transition') return false;\n if (typeof msg['from'] !== 'string' || typeof msg['to'] !== 'string') return false;\n if (msg['from'].length === 0 || msg['to'].length === 0) return false;\n if (msg['from'].length > MAX_STATE_LENGTH || msg['to'].length > MAX_STATE_LENGTH) return false;\n return true;\n}\n\n/**\n * Type-guard for `CounterMessage`.\n *\n * Rejects the payload if:\n * - `key` is not a non-empty string ≤ `MAX_STATE_LENGTH`\n * - `by` is not a finite number (guards against `Infinity`, `NaN`)\n */\nfunction isValidCounterMessage(msg: Record<string, unknown>): boolean {\n if (msg['type'] !== 'counter') return false;\n if (typeof msg['key'] !== 'string') return false;\n if (msg['key'].length === 0 || msg['key'].length > MAX_STATE_LENGTH) return false;\n if (typeof msg['by'] !== 'number' || !Number.isFinite(msg['by'])) return false;\n return true;\n}\n\n/**\n * Optional cross-tab synchronization layer using the\n * [BroadcastChannel API](https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel).\n *\n * **Design goals**\n *\n * 1. **Model consistency** — when a user navigates in Tab A, Tab B's Markov\n * graph and Bloom filter learn that transition too, so prefetch hints stay\n * accurate across a multi-tab session.\n *\n * 2. **Counter consistency** — when a user increments a named counter in Tab A\n * (e.g. `incrementCounter('articles_read')`), all other tabs receive the\n * same increment so their local counts stay in sync.\n *\n * 3. **XSS / model-poisoning hardening** — every incoming message is\n * validated before touching any data structure: state labels and counter\n * keys must be non-empty strings ≤ `MAX_STATE_LENGTH` (256 chars);\n * counter `by` values must be finite numbers.\n * Oversized or malformed payloads are silently dropped.\n *\n * 4. **Infinite-loop prevention** — remote transitions/increments applied via\n * `applyRemote()` / `applyRemoteCounter()` update in-memory state directly\n * **without** re-broadcasting to the channel.\n *\n * 5. **Bot flood containment** — `IntentManager` only calls `broadcast()` and\n * `broadcastCounter()` when the local `EntropyGuard` has *not* flagged the\n * session as a bot.\n *\n * **SSR / non-browser safety** — `BroadcastSync` checks for `BroadcastChannel`\n * availability at construction time. When it is absent (Node.js, older\n * browsers) the instance is created but all methods become no-ops; `isActive`\n * returns `false` so callers can inspect the state.\n *\n * @example\n * ```ts\n * const sync = new BroadcastSync('passiveintent-sync', graph, bloom, counters);\n * // Called by IntentManager after a verified local transition:\n * sync.broadcast('/home', '/products');\n * // Called by IntentManager after a local counter increment:\n * sync.broadcastCounter('articles_read', 1);\n * // Clean up on destroy:\n * sync.close();\n * ```\n */\nexport class BroadcastSync {\n private readonly channel: BroadcastChannel | null;\n private readonly graph: MarkovGraph;\n private readonly bloom: BloomFilter;\n private readonly counters: Map<string, number>;\n\n /**\n * `true` when a real `BroadcastChannel` was opened successfully.\n * `false` in SSR / environments without the API.\n */\n readonly isActive: boolean;\n\n constructor(\n channelName: string,\n graph: MarkovGraph,\n bloom: BloomFilter,\n counters: Map<string, number> = new Map(),\n ) {\n this.graph = graph;\n this.bloom = bloom;\n this.counters = counters;\n\n if (typeof BroadcastChannel === 'undefined') {\n this.channel = null;\n this.isActive = false;\n return;\n }\n\n this.channel = new BroadcastChannel(channelName);\n this.isActive = true;\n\n this.channel.onmessage = (event: MessageEvent) => {\n this.handleMessage(event.data);\n };\n }\n\n /**\n * Broadcast a locally-verified transition to all other tabs on the same\n * channel.\n *\n * **Must only be called for transitions that have already passed the local\n * `EntropyGuard` check.** `IntentManager` enforces this invariant; do not\n * call this method directly unless you can guarantee the same condition.\n *\n * @param from Normalized source state label (must not be empty).\n * @param to Normalized destination state label (must not be empty).\n */\n broadcast(from: string, to: string): void {\n if (!this.channel) return;\n const msg: TransitionMessage = { type: 'transition', from, to };\n this.channel.postMessage(msg);\n }\n\n /**\n * Broadcast a counter increment to all other tabs so their local counter\n * Maps stay in sync with the tab that originated the increment.\n *\n * **Must only be called when the local `EntropyGuard` has not flagged the\n * session as a bot.** `IntentManager.incrementCounter()` enforces this.\n *\n * @param key Counter key (must not be empty, ≤ MAX_STATE_LENGTH chars).\n * @param by Amount the counter was incremented (must be finite).\n */\n broadcastCounter(key: string, by: number): void {\n if (!this.channel) return;\n const msg: CounterMessage = { type: 'counter', key, by };\n this.channel.postMessage(msg);\n }\n\n /**\n * Apply a validated remote transition to the local in-memory model.\n *\n * This method updates the `MarkovGraph` and `BloomFilter` **without**\n * re-broadcasting the transition, which prevents the infinite-loop amplification\n * that would occur if received transitions were forwarded back to the channel.\n *\n * This is an internal method exposed for testing; production code should rely\n * on `onmessage` calling `handleMessage` automatically.\n *\n * @param from Validated source state label.\n * @param to Validated destination state label.\n */\n applyRemote(from: string, to: string): void {\n this.bloom.add(from);\n this.bloom.add(to);\n this.graph.incrementTransition(from, to);\n }\n\n /**\n * Apply a validated remote counter increment to the local counters Map\n * **without** re-broadcasting, preventing infinite-loop amplification.\n *\n * This is an internal method exposed for testing; production code should rely\n * on `onmessage` calling `handleMessage` automatically.\n *\n * @param key Validated counter key.\n * @param by Validated finite increment amount.\n */\n applyRemoteCounter(key: string, by: number): void {\n if (!this.counters.has(key) && this.counters.size >= 50) return;\n const current = this.counters.get(key) ?? 0;\n this.counters.set(key, current + by);\n }\n\n /**\n * Close the underlying `BroadcastChannel` and release the message handler.\n *\n * Call this inside `IntentManager.destroy()` to prevent ghost listeners.\n * Safe to call multiple times and in non-browser environments.\n */\n close(): void {\n if (!this.channel) return;\n this.channel.onmessage = null;\n this.channel.close();\n }\n\n // ── Private ──────────────────────────────────────────────────────────────\n\n /**\n * Validate and dispatch an incoming raw message from the broadcast channel.\n *\n * Security invariants:\n * - Non-object, null, and unrecognised `type` values are silently dropped.\n * - `TransitionMessage`: `from`/`to` must be non-empty strings ≤ `MAX_STATE_LENGTH`.\n * - `CounterMessage`: `key` must be a non-empty string ≤ `MAX_STATE_LENGTH`;\n * `by` must be a finite number.\n * - No re-broadcasting occurs — remote state is applied locally only.\n */\n private handleMessage(data: unknown): void {\n if (typeof data !== 'object' || data === null) return;\n const msg = data as Record<string, unknown>;\n if (msg['type'] === 'transition') {\n if (!isValidTransitionMessage(msg)) return;\n this.applyRemote(msg['from'] as string, msg['to'] as string);\n } else if (msg['type'] === 'counter') {\n if (!isValidCounterMessage(msg)) return;\n this.applyRemoteCounter(msg['key'] as string, msg['by'] as number);\n }\n // Unknown type values are silently ignored for forward-compatibility.\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport type { EnginePolicy } from './engine-policy.js';\nimport { BroadcastSync } from '../../sync/broadcast-sync.js';\nimport type { BloomFilter } from '../../core/bloom.js';\nimport type { MarkovGraph } from '../../core/markov.js';\n\n/**\n * Configuration for CrossTabSyncPolicy.\n */\nexport interface CrossTabSyncPolicyConfig {\n channelName: string;\n graph: MarkovGraph;\n bloom: BloomFilter;\n counters: Map<string, number>;\n /** Returns `true` when the EntropyGuard flags the session as a bot. */\n isSuspected: () => boolean;\n}\n\n/**\n * CrossTabSyncPolicy — broadcasts locally-verified transitions and counter\n * increments to other tabs via the BroadcastChannel API.\n *\n * Replaces the inline `if (this.broadcastSync && !this.signalEngine.suspected)`\n * conditionals that were previously in `IntentManager.runGraphAndSignalStage`\n * and `IntentManager.incrementCounter`.\n *\n * When this policy is **not** instantiated (because `crossTabSync` is\n * `false`), no broadcast logic executes at all.\n *\n * Security invariants (enforced by the underlying `BroadcastSync`):\n * - Incoming payloads are strictly validated (non-empty, ≤ 256 chars).\n * - Remote transitions are applied without re-broadcasting.\n * - Only non-bot sessions broadcast (guards bot-flood amplification).\n */\nexport class CrossTabSyncPolicy implements EnginePolicy {\n private readonly broadcastSync: BroadcastSync;\n private readonly isSuspected: () => boolean;\n\n constructor(config: CrossTabSyncPolicyConfig) {\n this.broadcastSync = new BroadcastSync(\n config.channelName,\n config.graph,\n config.bloom,\n config.counters,\n );\n this.isSuspected = config.isSuspected;\n }\n\n /**\n * Broadcast a transition after all signal evaluation has completed.\n * Skipped when the session is suspected as a bot.\n */\n onAfterEvaluation(from: string, to: string): void {\n // The `from` parameter name in the hook maps to the transition's source\n // state; `to` is the arriving state. We broadcast the original\n // (from, to) pair.\n // Note: this guard mirrors the original IntentManager conditional exactly.\n if (!this.isSuspected()) {\n this.broadcastSync.broadcast(from, to);\n }\n }\n\n /**\n * Broadcast a counter increment to other tabs.\n * Skipped when the session is suspected as a bot.\n */\n onCounterIncrement(key: string, by: number): void {\n if (!this.isSuspected()) {\n this.broadcastSync.broadcastCounter(key, by);\n }\n }\n\n /**\n * Close the underlying BroadcastChannel and release the message handler.\n */\n destroy(): void {\n this.broadcastSync.close();\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport { BenchmarkRecorder } from '../performance-instrumentation.js';\nimport type { PerformanceReport } from '../performance-instrumentation.js';\nimport { BrowserStorageAdapter, BrowserTimerAdapter } from '../adapters.js';\nimport type { AsyncStorageAdapter, StorageAdapter, TimerAdapter } from '../adapters.js';\nimport { BloomFilter } from '../core/bloom.js';\nimport { MarkovGraph } from '../core/markov.js';\nimport { normalizeRouteState } from '../utils/route-normalizer.js';\nimport type { SerializedMarkovGraph } from '../core/markov.js';\nimport type {\n ConversionPayload,\n PassiveIntentError,\n PassiveIntentTelemetry,\n IntentEventMap,\n IntentManagerConfig,\n} from '../types/events.js';\nimport { MAX_WINDOW_LENGTH } from './constants.js';\nimport { buildIntentManagerOptions } from './config-normalizer.js';\nimport { EventEmitter } from './event-emitter.js';\nimport { SignalEngine } from './signal-engine.js';\nimport { PersistenceCoordinator } from './persistence-coordinator.js';\nimport { LifecycleCoordinator } from './lifecycle-coordinator.js';\nimport type { EnginePolicy } from './policies/engine-policy.js';\nimport { DwellTimePolicy } from './policies/dwell-time-policy.js';\nimport { BigramPolicy } from './policies/bigram-policy.js';\nimport { DriftProtectionPolicy } from './policies/drift-protection-policy.js';\nimport { CrossTabSyncPolicy } from './policies/cross-tab-sync-policy.js';\n\n/**\n * Shared mutable context passed through each `trackStages` pipeline function.\n */\ninterface TrackContext {\n state: string;\n now: number;\n trackStart: number;\n from: string | null;\n isNewToBloom: boolean;\n}\n\n/**\n * Intent manager orchestrates collection + modeling + interventions.\n */\nexport class IntentManager {\n private readonly bloom: BloomFilter;\n private readonly graph: MarkovGraph;\n private readonly baseline: MarkovGraph | null;\n private readonly emitter = new EventEmitter<IntentEventMap>();\n private readonly benchmark: BenchmarkRecorder;\n private readonly timer: TimerAdapter;\n private readonly onError?: (error: PassiveIntentError) => void;\n private readonly botProtection: boolean;\n private readonly stateNormalizer?: (state: string) => string;\n\n /* Pluggable feature policies (deterministic order) */\n private readonly policies: EnginePolicy[];\n /** Index at which external plugin policies begin in `this.policies`. */\n private readonly pluginStartIndex: number;\n\n /* Collaborators */\n private readonly signalEngine: SignalEngine;\n private readonly persistenceCoordinator: PersistenceCoordinator;\n private readonly lifecycleCoordinator: LifecycleCoordinator;\n\n /* Pipeline state */\n private previousState: string | null = null;\n private previousStateEnteredAt: number = 0;\n private recentTrajectory: string[] = [];\n\n /* Deterministic named counters — session-scoped, never persisted */\n private counters = new Map<string, number>();\n\n /* GDPR-compliant telemetry */\n private readonly sessionId: string;\n private readonly assignmentGroup: 'treatment' | 'control';\n\n private readonly trackStages: Array<(ctx: TrackContext) => void>;\n\n constructor(config: IntentManagerConfig = {}) {\n // ── Normalize all config precedence, defaults, and clamping ────────────\n const opts = buildIntentManagerOptions(config);\n\n this.benchmark = new BenchmarkRecorder(config.benchmark);\n this.timer = config.timer ?? new BrowserTimerAdapter();\n this.onError = config.onError;\n this.botProtection = opts.botProtection;\n this.stateNormalizer = config.stateNormalizer;\n\n // Session ID — local-only, never transmitted.\n this.sessionId =\n typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'\n ? crypto.randomUUID()\n : Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);\n\n // A/B holdout assignment: randomly place the session in 'control' or 'treatment'.\n this.assignmentGroup = Math.random() * 100 < opts.holdoutPercent ? 'control' : 'treatment';\n\n // ── PersistenceCoordinator (restore on construction) ──────────────────────\n const persistenceCoordinator = new PersistenceCoordinator({\n storageKey: opts.storageKey,\n persistDebounceMs: opts.persistDebounceMs,\n persistThrottleMs: opts.persistThrottleMs,\n storage: config.storage ?? new BrowserStorageAdapter(),\n asyncStorage: config.asyncStorage ?? null,\n timer: this.timer,\n onError: config.onError,\n });\n this.persistenceCoordinator = persistenceCoordinator;\n\n const restored = persistenceCoordinator.restore(opts.graphConfig);\n\n this.bloom = restored?.bloom ?? new BloomFilter(config.bloom);\n this.graph = restored?.graph ?? new MarkovGraph(opts.graphConfig);\n this.baseline = config.baseline\n ? MarkovGraph.fromJSON(config.baseline, opts.graphConfig)\n : null;\n\n // Attach the live graph + bloom so the coordinator can serialise them.\n persistenceCoordinator.attach(this.graph, this.bloom);\n\n // ── Policies (deterministic order) ─────────────────────────────────────────\n // Each policy is only instantiated when its feature flag is enabled.\n // Policies are called in array order at each hook point.\n const driftPolicy = new DriftProtectionPolicy(\n opts.driftMaxAnomalyRate,\n opts.driftEvaluationWindowMs,\n );\n const policies: EnginePolicy[] = [driftPolicy];\n\n // ── SignalEngine ──────────────────────────────────────────────────────────\n this.signalEngine = new SignalEngine({\n graph: this.graph,\n baseline: this.baseline,\n timer: this.timer,\n benchmark: this.benchmark,\n emitter: this.emitter,\n assignmentGroup: this.assignmentGroup,\n eventCooldownMs: opts.eventCooldownMs,\n dwellTimeMinSamples: opts.dwellTimeMinSamples,\n dwellTimeZScoreThreshold: opts.dwellTimeZScoreThreshold,\n hesitationCorrelationWindowMs: opts.hesitationCorrelationWindowMs,\n trajectorySmoothingEpsilon: opts.trajectorySmoothingEpsilon,\n driftPolicy,\n });\n\n // DwellTimePolicy — only when dwell detection is enabled.\n if (opts.dwellTimeEnabled) {\n policies.push(\n new DwellTimePolicy({\n isSuspected: () => this.signalEngine.suspected,\n evaluateDwellTime: (state, dwellMs) => {\n this.signalEngine.dispatch(this.signalEngine.evaluateDwellTime(state, dwellMs));\n },\n getPreviousStateEnteredAt: () => this.previousStateEnteredAt,\n emitter: this.emitter,\n }),\n );\n }\n\n // BigramPolicy — only when second-order transitions are enabled.\n if (opts.enableBigrams) {\n policies.push(new BigramPolicy(this.graph, opts.bigramFrequencyThreshold));\n }\n\n // CrossTabSyncPolicy — only when cross-tab sync is enabled.\n if (opts.crossTabSync) {\n policies.push(\n new CrossTabSyncPolicy({\n channelName: `passiveintent-sync:${opts.storageKey}`,\n graph: this.graph,\n bloom: this.bloom,\n counters: this.counters,\n isSuspected: () => this.signalEngine.suspected,\n }),\n );\n }\n\n this.pluginStartIndex = policies.length;\n this.policies = [...policies, ...opts.plugins];\n\n // ── LifecycleCoordinator ──────────────────────────────────────────────────\n this.lifecycleCoordinator = new LifecycleCoordinator({\n lifecycleAdapter: config.lifecycleAdapter,\n timer: this.timer,\n dwellTimeEnabled: opts.dwellTimeEnabled,\n emitter: this.emitter,\n onAdjustBaseline: (delta: number) => {\n this.previousStateEnteredAt += delta;\n },\n onResetBaseline: () => {\n this.previousStateEnteredAt = this.timer.now();\n },\n hasPreviousState: () => this.previousState !== null,\n getPreviousState: () => this.previousState,\n // ── Smart Exit-Intent ──────────────────────────────────────────────────\n // Only fires when the Markov graph indicates a likely continuation path\n // from the current state. The check is performed here (in IntentManager)\n // rather than in LifecycleCoordinator so that the coordinator stays\n // decoupled from graph math.\n onExitIntent: () => {\n if (this.previousState === null) return;\n const candidates = this.graph.getLikelyNextStates(this.previousState, 0.4);\n if (candidates.length === 0) return;\n this.emitter.emit('exit_intent', {\n state: this.previousState,\n likelyNext: candidates[0].state,\n });\n },\n });\n\n // ── Pipeline stages ───────────────────────────────────────────────────────\n // Stages are bound arrow functions so future versions can insert, replace, or\n // reorder steps without touching the core track() loop.\n this.trackStages = [\n this.runBotProtectionStage,\n this.runBloomStage,\n this.runTransitionContextStage,\n this.runGraphAndSignalStage,\n this.runEmitAndPersistStage,\n ];\n }\n\n /**\n * Invoke `fn` for policy at index `i`. Calls from built-in policies\n * (index < pluginStartIndex) run unwrapped; external plugins are wrapped in\n * a try/catch so a throwing plugin never propagates into the host application.\n */\n private callPolicy(i: number, fn: () => void): void {\n if (i < this.pluginStartIndex) {\n fn();\n } else {\n try {\n fn();\n } catch (err) {\n if (this.onError) {\n this.onError({\n code: 'VALIDATION',\n message: `IntentManager: plugin[${i - this.pluginStartIndex}] threw: ${err instanceof Error ? err.message : String(err)}`,\n originalError: err,\n });\n }\n }\n }\n }\n\n on<K extends keyof IntentEventMap>(\n event: K,\n listener: (payload: IntentEventMap[K]) => void,\n ): () => void {\n return this.emitter.on(event, listener);\n }\n\n /**\n * Async factory for environments with asynchronous storage backends\n * (React Native AsyncStorage, Capacitor Preferences, IndexedDB wrappers, etc.).\n *\n * `createAsync` awaits the initial `getItem` call to pre-load any persisted\n * Bloom filter + Markov graph **before** constructing the engine, so that\n * the synchronous `track()` hot-path is never blocked by I/O. Once the\n * initial read completes, the engine is instantiated synchronously using\n * a lightweight bridge adapter that vends the pre-read payload to\n * `restore()` — no further synchronous storage access is performed.\n *\n * Subsequent `persist()` calls use `config.asyncStorage.setItem()` in a\n * fire-and-forget manner, guarded by an in-flight write flag to prevent\n * overlapping writes.\n *\n * ```ts\n * const adapter: AsyncStorageAdapter = {\n * getItem: (key) => AsyncStorage.getItem(key),\n * setItem: (key, value) => AsyncStorage.setItem(key, value),\n * };\n * const intent = await IntentManager.createAsync({ asyncStorage: adapter });\n * ```\n *\n * @throws {Error} When `config.asyncStorage` is not provided.\n */\n static async createAsync(config: IntentManagerConfig): Promise<IntentManager> {\n if (!config.asyncStorage) {\n throw new Error('IntentManager.createAsync() requires config.asyncStorage');\n }\n // Use the same default storage key as the config normalizer without\n // incurring a second full normalization pass.\n const storageKey = config.storageKey ?? 'passive-intent';\n // Await the single I/O call up-front so the constructor stays synchronous.\n const raw = await config.asyncStorage.getItem(storageKey);\n\n // Build a minimal sync bridge that serves the pre-read payload to restore()\n // and is otherwise a no-op for setItem (async writes go through asyncStorage\n // inside persist()).\n // Note: `storage` is explicitly omitted from the spread so that if the\n // caller also set `config.storage`, we don't inadvertently trigger the\n // \"both adapters provided\" warning in the constructor.\n const preloadBridge: StorageAdapter = {\n // getItem is invoked exactly once — by restore() in the constructor.\n getItem: () => raw,\n setItem: () => {\n /* writes handled async in persist() */\n },\n };\n const { storage: _omit, ...restConfig } = config;\n return new IntentManager({ ...restConfig, storage: preloadBridge });\n }\n\n /**\n * Track a page view or custom state transition.\n *\n * The `state` argument is automatically normalized via `normalizeRouteState()`\n * before any processing. This means you can pass raw URL strings directly —\n * query strings, hash fragments, trailing slashes, UUIDs, numeric IDs and MongoDB\n * ObjectIDs are all stripped or replaced so the engine always receives a\n * stable, canonical state label.\n *\n * ```ts\n * intent.track('/users/550e8400-e29b-41d4-a716-446655440000/profile?tab=bio');\n * // internally treated as: '/users/:id/profile'\n * ```\n */\n track(state: string): void {\n // Normalise first: strip query strings, hash fragments, trailing slashes,\n // and replace dynamic ID segments (UUIDs, MongoDB ObjectIDs, numeric IDs) with ':id'.\n state = normalizeRouteState(state);\n\n // Apply optional custom normalizer (e.g. for SEO slugs).\n if (this.stateNormalizer) {\n try {\n const normalized = this.stateNormalizer(state);\n const coerced = String(normalized);\n // Empty string is a deliberate \"skip this state\" signal from the\n // normalizer — drop silently without firing a VALIDATION error.\n if (coerced === '') return;\n state = coerced;\n } catch (err) {\n if (this.onError) {\n this.onError({\n code: 'VALIDATION',\n message: `IntentManager.track(): stateNormalizer threw: ${err instanceof Error ? err.message : String(err)}`,\n });\n }\n return;\n }\n }\n\n // Guard: '' is reserved internally as a tombstone marker.\n // Silently drop and surface a non-fatal error rather than crashing the host.\n if (state === '') {\n if (this.onError) {\n this.onError({\n code: 'VALIDATION',\n message: 'IntentManager.track(): state label must not be an empty string',\n });\n }\n return;\n }\n\n const now = this.timer.now();\n const trackStart = this.benchmark.now();\n\n // Advance drift-protection rolling window via policy hooks (O(1), no allocations)\n for (let i = 0; i < this.policies.length; i += 1)\n this.callPolicy(i, () => this.policies[i].onTrackStart?.(now));\n\n const ctx: TrackContext = {\n state,\n now,\n trackStart,\n from: null,\n isNewToBloom: false,\n };\n\n for (let i = 0; i < this.trackStages.length; i += 1) {\n this.trackStages[i](ctx);\n }\n\n this.benchmark.record('track', trackStart);\n }\n\n private runBotProtectionStage = (ctx: TrackContext): void => {\n if (!this.botProtection) return;\n const botResult = this.signalEngine.recordBotCheck(ctx.now);\n if (botResult.transitionedToBot) {\n this.emitter.emit('bot_detected', { state: ctx.state });\n }\n };\n\n private runBloomStage = (ctx: TrackContext): void => {\n ctx.isNewToBloom = !this.bloom.check(ctx.state);\n const bloomAddStart = this.benchmark.now();\n this.bloom.add(ctx.state);\n this.benchmark.record('bloomAdd', bloomAddStart);\n };\n\n private runTransitionContextStage = (ctx: TrackContext): void => {\n ctx.from = this.previousState;\n this.previousState = ctx.state;\n\n // Dwell-time measurement — delegated to DwellTimePolicy when enabled.\n for (let i = 0; i < this.policies.length; i += 1)\n this.callPolicy(i, () => this.policies[i].onTrackContext?.(ctx));\n\n // CONTRACT: this reset MUST remain unconditional and MUST happen after all\n // onTrackContext hooks. DwellTimePolicy reads previousStateEnteredAt inside\n // its hook and relies on this line to clear any stale baseline afterwards —\n // including the session_stale (dwell_exceeded) code path.\n this.previousStateEnteredAt = ctx.now;\n\n this.recentTrajectory.push(ctx.state);\n if (this.recentTrajectory.length > MAX_WINDOW_LENGTH) this.recentTrajectory.shift();\n };\n\n private runGraphAndSignalStage = (ctx: TrackContext): void => {\n // Skip graph updates when the session is flagged as a bot.\n // This prevents automation / scrapers from poisoning the Markov\n // transition probabilities that drive real-user predictions.\n if (this.botProtection && this.signalEngine.suspected) {\n // Bloom updates from runBloomStage still need to be persisted even when\n // graph writes are skipped. hasSeen() is a public API that should stay\n // consistent across session reloads: if a URL was visited (even during a\n // suspected-bot burst), it should remain \"seen\" after the page reloads.\n // Only the Markov graph is protected from bot poisoning, not bloom membership.\n if (ctx.isNewToBloom) this.persistenceCoordinator.markDirty();\n return;\n }\n\n if (ctx.from) {\n const incrementStart = this.benchmark.now();\n this.graph.incrementTransition(ctx.from, ctx.state);\n this.benchmark.record('incrementTransition', incrementStart);\n\n // Increment transition counter in SignalEngine\n this.signalEngine.recordTransition(ctx.from, ctx.state, this.recentTrajectory);\n\n // Bigram accounting — delegated to BigramPolicy when enabled.\n for (let i = 0; i < this.policies.length; i += 1)\n this.callPolicy(i, () =>\n this.policies[i].onTransition?.(ctx.from!, ctx.state, this.recentTrajectory),\n );\n\n this.persistenceCoordinator.markDirty();\n this.signalEngine.dispatch(this.signalEngine.evaluateEntropy(ctx.state));\n this.signalEngine.dispatch(\n this.signalEngine.evaluateTrajectory(ctx.from, ctx.state, this.recentTrajectory),\n );\n\n // Cross-tab broadcast — delegated to CrossTabSyncPolicy when enabled.\n for (let i = 0; i < this.policies.length; i += 1)\n this.callPolicy(i, () => this.policies[i].onAfterEvaluation?.(ctx.from!, ctx.state));\n\n return;\n }\n\n if (ctx.isNewToBloom) {\n this.persistenceCoordinator.markDirty();\n }\n };\n\n private runEmitAndPersistStage = (ctx: TrackContext): void => {\n this.emitter.emit('state_change', { from: ctx.from, to: ctx.state });\n // Synchronous persist on every transition — crash-safe against sudden OS\n // process kills where lifecycle events never fire.\n // The dirty-flag short-circuit keeps this a no-op when nothing changed.\n this.persistenceCoordinator.persist();\n };\n\n /**\n * @internal Test-only accessor for the dwell-clock baseline.\n * Not part of the stable public API — prefixed with `_` to signal that.\n */\n get _previousStateEnteredAt(): number {\n return this.previousStateEnteredAt;\n }\n\n hasSeen(state: string): boolean {\n const start = this.benchmark.now();\n const seen = this.bloom.check(state);\n this.benchmark.record('bloomCheck', start);\n return seen;\n }\n\n /**\n * Reset session-specific state for clean evaluation boundaries.\n * Clears the recent trajectory and previous state, but preserves\n * the learned Markov graph and Bloom filter.\n */\n resetSession(): void {\n this.recentTrajectory = [];\n this.previousState = null;\n this.previousStateEnteredAt = 0;\n }\n\n exportGraph(): SerializedMarkovGraph {\n return this.graph.toJSON();\n }\n\n /**\n * Returns the most likely next states from the current (or previous) state,\n * filtered by a minimum probability threshold and an optional sanitize predicate.\n *\n * Designed for **read-only** UI prefetching hints only. This method exposes\n * predictive data from the Markov graph to the host application so it can\n * preload assets or warm caches for the most probable next routes.\n *\n * ⚠ **Security constraint — you MUST provide a `sanitize` function.**\n * Without a sanitize predicate, the returned list may include state-mutating\n * or privacy-sensitive routes such as `/logout`, `/checkout/pay`, or routes\n * that embed PII (e.g. `/users/john.doe/settings`). The sanitize function\n * must return `false` for any such route. Prefetching must **never** trigger\n * state-mutating side effects — treat the results as navigation hints only.\n *\n * ```ts\n * // ✅ Safe usage with a sanitize guard\n * const hints = intent.predictNextStates(0.3, (state) => {\n * const blocked = ['/logout', '/checkout/pay', '/delete-account'];\n * return !blocked.some((b) => state.startsWith(b)) &&\n * !/\\/users\\/[^/]+\\/pii/.test(state);\n * });\n * hints.forEach(({ state, probability }) => prefetch(state));\n * ```\n *\n * @param threshold Minimum probability in [0, 1] for a state to be included.\n * Defaults to `0.3`.\n * @param sanitize Optional predicate that receives each candidate state label\n * and returns `true` to **include** it or `false` to **exclude**\n * it. When omitted all states above the threshold are returned,\n * which is **unsafe** for production use — always supply this.\n * @returns Filtered and sorted `{ state, probability }[]`, descending by\n * probability. Returns an empty array when no previous state is known\n * or no transitions meet the threshold.\n */\n predictNextStates(\n threshold = 0.3,\n sanitize?: (state: string) => boolean,\n ): { state: string; probability: number }[] {\n if (this.previousState === null) return [];\n const candidates = this.graph.getLikelyNextStates(this.previousState, threshold);\n if (!sanitize) return candidates;\n return candidates.filter(({ state }) => sanitize(state));\n }\n\n flushNow(): void {\n this.persistenceCoordinator.flushNow();\n }\n\n /**\n * Tear down the manager: flush any pending state to storage,\n * cancel the debounce timer, and remove all event listeners.\n *\n * Call this in SPA cleanup paths (React `useEffect` teardown,\n * Vue `onUnmounted`, Angular `ngOnDestroy`) to prevent memory\n * leaks from retained listener references.\n *\n * After `destroy()` the instance should be discarded.\n */\n destroy(): void {\n this.persistenceCoordinator.flushNow(); // best-effort final write (may be async)\n this.persistenceCoordinator.close(); // prevent post-destroy timer re-arm\n this.emitter.removeAll();\n this.lifecycleCoordinator.destroy();\n for (let i = 0; i < this.policies.length; i += 1)\n this.callPolicy(i, () => this.policies[i].destroy?.());\n }\n\n /**\n * Returns a GDPR-compliant telemetry snapshot for the current session.\n *\n * All fields are aggregate counters or derived status flags.\n * No raw behavioral data, no state labels, and no user-identifying\n * information is included. Safe to send to your own analytics endpoint\n * without triggering GDPR personal-data obligations.\n *\n * ```ts\n * const t = intent.getTelemetry();\n * // { sessionId: 'a1b2...', transitionsEvaluated: 42, botStatus: 'human',\n * // anomaliesFired: 3, engineHealth: 'healthy' }\n * ```\n */\n getTelemetry(): PassiveIntentTelemetry {\n return {\n sessionId: this.sessionId,\n transitionsEvaluated: this.signalEngine.transitionsEvaluated,\n botStatus: this.signalEngine.suspected ? 'suspected_bot' : 'human',\n anomaliesFired: this.signalEngine.anomaliesFired,\n engineHealth: this.persistenceCoordinator.engineHealth,\n baselineStatus: this.signalEngine.baselineStatus,\n assignmentGroup: this.assignmentGroup,\n };\n }\n\n /**\n * Record a conversion event and emit it through the event bus.\n *\n * Use this to measure the ROI of intent-driven interventions (e.g.\n * whether a hesitation discount actually led to a purchase).\n *\n * ```ts\n * intent.on('conversion', ({ type, value, currency }) => {\n * // All local — log to your own backend if needed\n * console.log(`Conversion: ${type} ${value} ${currency}`);\n * });\n *\n * // After a purchase completes:\n * intent.trackConversion({ type: 'purchase', value: 49.99, currency: 'USD' });\n * ```\n *\n * **Privacy note:** `type` must not contain user identifiers.\n * This event never leaves the device unless your `conversion` listener\n * explicitly sends it — which remains entirely under your control.\n */\n trackConversion(payload: ConversionPayload): void {\n this.emitter.emit('conversion', payload);\n }\n\n /**\n * Increment a named counter by `by` (default 1) and return the new value.\n *\n * Counters are deterministic: unlike the probabilistic Bloom filter, they\n * track exact counts with no false positives. Use them for business\n * metrics such as \"articles read\", \"items added to cart\", or any case\n * where an exact integer matters.\n *\n * Counters are session-scoped and never persisted to storage.\n *\n * ```ts\n * intent.incrementCounter('articles_read'); // 1\n * intent.incrementCounter('articles_read'); // 2\n * intent.incrementCounter('articles_read', 3); // 5\n * ```\n *\n * @param key - Identifier for the counter. Must not be an empty string.\n * @param by - Amount to add. Defaults to 1. Must be a finite number.\n * @returns The new counter value after incrementing.\n */\n incrementCounter(key: string, by = 1): number {\n if (key === '') {\n if (this.onError) {\n this.onError({\n code: 'VALIDATION',\n message: 'IntentManager.incrementCounter(): key must not be an empty string',\n });\n }\n return 0;\n }\n if (!Number.isFinite(by)) {\n if (this.onError) {\n this.onError({\n code: 'VALIDATION',\n message: `IntentManager.incrementCounter(): 'by' must be a finite number, got ${by}`,\n });\n }\n return this.counters.get(key) ?? 0;\n }\n if (!this.counters.has(key) && this.counters.size >= 50) {\n if (this.onError) {\n this.onError({\n code: 'LIMIT_EXCEEDED',\n message: 'IntentManager.incrementCounter(): max unique counter keys (50) reached',\n });\n }\n return 0;\n }\n const next = (this.counters.get(key) ?? 0) + by;\n this.counters.set(key, next);\n // Broadcast the increment to other tabs via CrossTabSyncPolicy.\n for (let i = 0; i < this.policies.length; i += 1)\n this.callPolicy(i, () => this.policies[i].onCounterIncrement?.(key, by));\n return next;\n }\n\n /**\n * Return the current value of a named counter, or 0 if it has never been\n * incremented.\n *\n * ```ts\n * intent.getCounter('articles_read'); // 0 before any increments\n * ```\n *\n * @param key - Identifier for the counter.\n */\n getCounter(key: string): number {\n return this.counters.get(key) ?? 0;\n }\n\n /**\n * Reset a named counter to 0.\n *\n * After this call `getCounter(key)` returns 0. The counter entry is\n * removed from internal storage rather than being set to 0, keeping the\n * map compact.\n *\n * ```ts\n * intent.incrementCounter('articles_read', 5);\n * intent.resetCounter('articles_read');\n * intent.getCounter('articles_read'); // 0\n * ```\n *\n * @param key - Identifier for the counter to reset.\n */\n resetCounter(key: string): void {\n this.counters.delete(key);\n }\n\n getPerformanceReport(): PerformanceReport {\n const serialized = this.graph.toJSON();\n return this.benchmark.report({\n stateCount: this.graph.stateCount(),\n totalTransitions: this.graph.totalTransitions(),\n bloomBitsetBytes: this.bloom.getBitsetByteSize(),\n serializedGraphBytes: this.benchmark.serializedSizeBytes(serialized),\n });\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport type { IStateModel } from '../types/microkernel.js';\n\n// ---------------------------------------------------------------------------\n// Internal BFS types\n// ---------------------------------------------------------------------------\n\n/**\n * A single node on the BFS frontier during the hitting-probability walk.\n *\n * Declared inline (not a class) so V8 can allocate it cheaply as a plain\n * object literal. The BFS queue is short-lived and GC'd after `updateBaseline`\n * returns, so allocation cost is irrelevant on the hot path.\n */\ninterface BFSNode {\n /** Normalised route label at this position in the walk. */\n readonly state: string;\n /**\n * Product of all edge probabilities from the source state to this node.\n *\n * At the source node this is 1.0 (multiplicative identity). Each hop\n * multiplies by the Markov transition probability P(nextState | state),\n * producing the joint probability of the entire path up to this point.\n */\n readonly pathProb: number;\n /** Number of hops taken from the source state to this node. */\n readonly depth: number;\n /**\n * States already visited on this specific path from the source to this node.\n * Tracked per-path (not globally) so that converging simple paths are not\n * incorrectly pruned: two different paths may visit the same intermediate\n * state independently without forming a cycle.\n */\n readonly pathVisited: ReadonlySet<string>;\n}\n\n// ---------------------------------------------------------------------------\n// PropensityCalculator\n// ---------------------------------------------------------------------------\n\n/**\n * Real-Time Propensity Calculator\n *\n * Produces a single normalised score in [0, 1] answering the question:\n * \"How likely is the current user session to reach `targetState` given\n * the observed behavioural friction so far?\"\n *\n * The score is the product of two independent, orthogonal factors:\n *\n * ─────────────────────────────────────────────────────────────────────\n * Factor 1 — Markov hitting probability (structural, graph-derived)\n * ─────────────────────────────────────────────────────────────────────\n *\n * P_reach = Σ ∏ P(s_{i+1} | s_i)\n * ∀ simple paths currentState → … → targetState\n * of length 1 … maxDepth\n *\n * Computed once by a depth-bounded BFS over the live Markov graph and\n * cached in `cachedBaseline`. Separating this from the Z-score means the\n * hot path (`getRealTimePropensity`) never re-traverses the graph.\n *\n * ─────────────────────────────────────────────────────────────────────\n * Factor 2 — Welford Z-score friction penalty (behavioural, real-time)\n * ─────────────────────────────────────────────────────────────────────\n *\n * frictionPenalty = exp(−α × max(0, z))\n *\n * Derived from the trajectory Z-score emitted by `SignalEngine.evaluateTrajectory`\n * (itself computed via Welford online variance over the log-likelihood series).\n * A high positive z means the current path deviates significantly from the\n * calibrated baseline — the user is confused or frustrated — which reduces\n * the probability of a clean conversion.\n *\n * `max(0, z)` clamps negative Z-scores to zero: a trajectory that is *more*\n * likely than baseline (smooth, decisive navigation) must not inflate the\n * propensity above the raw structural probability.\n *\n * ─────────────────────────────────────────────────────────────────────\n * Combined score\n * ─────────────────────────────────────────────────────────────────────\n *\n * propensity = P_reach × exp(−α × max(0, z))\n *\n * At the default α = 0.2:\n * z = 0 → penalty = 1.000 (no friction — full structural propensity)\n * z = 3.5 → penalty ≈ 0.497 (divergence threshold — score halved)\n * z = 6.9 → penalty ≈ 0.250 (severe anomaly — score quartered)\n *\n * ─────────────────────────────────────────────────────────────────────\n * Performance contract\n * ─────────────────────────────────────────────────────────────────────\n *\n * `getRealTimePropensity()` — throttled to ≤ 1 full computation per\n * THROTTLE_MS (default 500 ms). Throttled calls: zero allocations,\n * one `performance.now()` read, one float comparison, one return.\n * Full computation: one `performance.now()`, one `Math.exp()`, one\n * multiply. Runs comfortably under 1 µs on V8.\n *\n * `updateBaseline()` — O(D × F) BFS where D = maxDepth and F = average\n * graph fan-out. At D = 3, F ≤ 8 this is ≤ 512 frontier nodes.\n * Must NOT be called on every `track()` — only when the\n * (currentState, targetState) pair changes.\n *\n * Zero external dependencies. No network I/O. No PII.\n */\nexport class PropensityCalculator {\n /**\n * Exponential decay sensitivity constant α.\n *\n * Controls how sharply anomalous Z-scores suppress the propensity score.\n *\n * frictionPenalty = exp(−α × z)\n *\n * Default 0.2 is calibrated against the library's default `divergenceThreshold`\n * of 3.5: at that threshold the score is halved, providing a meaningful\n * real-time signal without over-penalising brief navigation anomalies.\n *\n * Increase α (e.g., 0.4) for higher sensitivity in short funnel flows.\n * Decrease α (e.g., 0.1) for longer, noisier browsing sessions.\n */\n private readonly alpha: number;\n\n /**\n * Cached Markov hitting probability from the most recent `updateBaseline()` call.\n *\n * Stores the sum of joint path probabilities over all simple paths of\n * length 1 … maxDepth that connect `currentState` to `targetState` in the\n * live Markov graph. Clamped to [0, 1] by `updateBaseline`.\n *\n * A value of 0 signals one of:\n * (a) `updateBaseline()` has never been called in this session.\n * (b) `targetState` is structurally unreachable from `currentState`\n * within `maxDepth` hops given the observed transition history.\n */\n private cachedBaseline: number;\n\n /**\n * `performance.now()` timestamp of the last accepted propensity computation.\n *\n * Compared against the current timestamp on each `getRealTimePropensity()`\n * call to enforce the THROTTLE_MS gate.\n *\n * Initialized to `-Infinity` so the very first call always satisfies\n * `now - lastCalculationTime ≥ THROTTLE_MS` regardless of what\n * `performance.now()` returns — including 0 in controlled test environments.\n * Using `0` would throttle the first call whenever the clock also starts at 0.\n */\n private lastCalculationTime: number;\n\n /**\n * The propensity score produced by the most recent full (non-throttled) computation.\n *\n * Returned as-is on all subsequent calls within the THROTTLE_MS window,\n * avoiding redundant `Math.exp()` evaluations when the caller polls faster\n * than the throttle interval.\n */\n private lastPropensity: number;\n\n /**\n * Minimum elapsed time in milliseconds between full propensity re-computations.\n *\n * 500 ms was chosen to:\n * • Align with the dwell-time sampling cadence so the score only updates\n * when new dwell-time evidence has likely been collected.\n * • Prevent score oscillation in React consumers: a 500 ms stable window\n * maps to a single React render cycle at typical re-render rates.\n * • Stay well above the p99 `track()` latency (1.6 µs in benchmarks), so\n * throttling never masks a meaningful computation.\n */\n private readonly THROTTLE_MS: number;\n\n constructor(alpha: number = 0.2, throttleMs: number = 500) {\n // Negative alpha inverts the friction relationship (higher z → higher score),\n // and non-finite alpha (NaN or ±Infinity) causes Math.exp to produce NaN at\n // z=0 via Infinity×0. Clamp to [0, ∞) finite; 0 means no friction applied.\n this.alpha = Number.isFinite(alpha) && alpha >= 0 ? alpha : 0;\n this.cachedBaseline = 0;\n this.lastCalculationTime = -Infinity;\n this.lastPropensity = 0;\n // NaN throttleMs disables throttling silently (n < NaN is always false).\n // Infinity throttleMs freezes the score after the first computation forever.\n // Both are hazards; fall back to the documented 500 ms default.\n this.THROTTLE_MS = Number.isFinite(throttleMs) && throttleMs >= 0 ? throttleMs : 500;\n }\n\n // -------------------------------------------------------------------------\n // Public API\n // -------------------------------------------------------------------------\n\n /**\n * Recompute and cache the structural hitting probability via a depth-bounded BFS.\n *\n * ### Why BFS over DFS?\n * BFS explores the graph level-by-level (by hop count), making it natural to\n * accumulate probability mass per depth and terminate cleanly at `maxDepth`.\n * With iterative DFS an explicit depth counter is needed alongside the stack;\n * BFS's queue subsumes both responsibilities and reads more clearly for a\n * probabilistic forward walk.\n *\n * ### Cycle prevention\n * Each queued node carries its own `pathVisited` set — the states already on\n * that specific path from `currentState` to this node. Before enqueuing a\n * neighbor we check `pathVisited` and skip it if it appears, preventing the\n * BFS from following cycles (A→B→A→…) along any single route. Unlike a\n * global visited set, this approach allows two *different* paths to pass\n * through the same intermediate state independently, which is required to\n * correctly sum all simple-path contributions to the hitting probability.\n *\n * ### Probability accumulation\n * Each BFS node carries `pathProb` — the running product of edge probabilities\n * from the source. When a neighbour is `targetState`, we add\n * `pathProb × P(target | state)` to `accumulated` and do NOT enqueue\n * `targetState` for further expansion — once the target is reached, the path\n * has terminated. This correctly handles multiple paths of different lengths\n * that all converge on the target.\n *\n * @param graph The live `IStateModel`. Only `getLikelyNext` is used,\n * so any conforming implementation (real graph, test stub)\n * works without change.\n * @param currentState The user's current observed state (normalised route string).\n * @param targetState The goal state whose reachability we are estimating\n * (e.g., `/checkout/confirm`, `/onboarding/complete`).\n * @param maxDepth Maximum BFS hops. Default 3. At fan-out 8 the frontier\n * is ≤ 8^3 = 512 nodes — safe for a synchronous call.\n * Values above 5 are not recommended: at fan-out 8, depth 5\n * yields 32 768 nodes and adds measurable latency.\n */\n public updateBaseline(\n graph: IStateModel,\n currentState: string,\n targetState: string,\n maxDepth: number = 3,\n ): void {\n // ── Input sanitization ────────────────────────────────────────────────────\n // NaN maxDepth: `depth + 1 < NaN` is always false, so non-target neighbors\n // are never enqueued — only direct target edges are found, silently ignoring\n // the requested depth. Infinity maxDepth removes the depth gate entirely,\n // risking unbounded BFS in dense graphs. Fall back to the documented default.\n const safeMaxDepth = Number.isFinite(maxDepth) && maxDepth >= 1 ? Math.floor(maxDepth) : 3;\n\n // ── Trivial case: the user is already at the target ───────────────────────\n // Markov hitting probability from a state to itself is 1 by definition —\n // the chain has already hit the absorbing target state.\n if (currentState === targetState) {\n this.cachedBaseline = 1;\n return;\n }\n\n // ── BFS initialisation ────────────────────────────────────────────────────\n // The queue holds frontier nodes in FIFO order.\n // `pathVisited` is tracked per-path so that converging simple paths are not\n // incorrectly pruned: two routes may share an intermediate state without\n // forming a cycle. A global visited set would drop valid second arrivals.\n const queue: BFSNode[] = [\n { state: currentState, pathProb: 1, depth: 0, pathVisited: new Set([currentState]) },\n ];\n\n // Running sum of joint path probabilities that terminate at `targetState`.\n let accumulated = 0;\n\n while (queue.length > 0) {\n // `shift()` is O(n) but the queue is bounded by the number of distinct\n // simple paths: at fan-out F and depth D that is at most F×(F-1)^(D-1).\n // At D=3, F=8 this is ~400 entries — a ring-buffer would save nothing.\n const node = queue.shift()!;\n\n // ── Expand: enumerate all outgoing transitions from this state ──────────\n // Threshold 0 returns every observed edge regardless of probability.\n // We intentionally do not filter: even a 1% edge can compound into a\n // meaningful multi-hop path, and silently dropping low-probability edges\n // would understate reachability.\n const neighbours = graph.getLikelyNext(node.state, 0);\n\n for (const { state: nextState, probability: edgeProb } of neighbours) {\n // Skip states already on this path to prevent cycles.\n if (node.pathVisited.has(nextState)) {\n continue;\n }\n\n // Joint probability of the path ending at `nextState`:\n // pathProb(node → nextState) = pathProb(source → node) × P(nextState | node)\n const reachProb = node.pathProb * edgeProb;\n\n if (nextState === targetState) {\n // ── Target reached — accumulate and continue ────────────────────────\n // Do NOT enqueue `targetState`: we only need to count arrivals,\n // not expand from the goal. Multiple paths of different lengths\n // can hit the target, so we accumulate rather than early-return.\n accumulated += reachProb;\n } else if (node.depth + 1 < safeMaxDepth) {\n // ── Not yet at target and depth budget remains — keep walking ────────\n // Push a shallow copy of pathVisited with nextState added so each\n // queued node carries its own independent path history.\n const nextPathVisited = new Set(node.pathVisited);\n nextPathVisited.add(nextState);\n queue.push({\n state: nextState,\n pathProb: reachProb,\n depth: node.depth + 1,\n pathVisited: nextPathVisited,\n });\n }\n // States beyond maxDepth are silently discarded —\n // they cannot improve the estimate without risking cycle inflation.\n }\n }\n\n // Clamp to [0, 1]: by the Markov chain probability axioms, the hitting\n // probability is in [0, 1]. Floating-point products across many parallel\n // paths can accumulate rounding error beyond 1.0 in degenerate graphs;\n // the clamp is a correctness safety net, not the normal code path.\n this.cachedBaseline = Math.min(1, accumulated);\n }\n\n /**\n * Return the real-time propensity score, throttled to at most one full\n * computation per `THROTTLE_MS` milliseconds.\n *\n * ### Formula\n *\n * frictionPenalty = exp(−α × max(0, currentZScore))\n * propensity = cachedBaseline × frictionPenalty\n *\n * ### Why exponential decay for the friction penalty?\n *\n * • It maps every non-negative Z-score to a unique value in (0, 1],\n * preserving the [0, 1] domain without a separate clamp.\n * • It is monotonically decreasing — higher friction always reduces propensity.\n * • It is C∞ differentiable — smooth score transitions eliminate visual\n * jitter in React consumers that read the score via `usePassiveIntent`.\n * • It requires a single `Math.exp()` call — < 100 ns on V8.\n * • The `max(0, z)` clamp ensures negative Z-scores (the user navigates\n * *better* than baseline) produce no friction, keeping the score at\n * `cachedBaseline` rather than inflating it above the Markov probability.\n *\n * ### Throttle mechanics\n *\n * `performance.now()` provides sub-millisecond resolution without OS-level\n * privileges (resolution: ~0.1 ms in secure browser contexts, ~1 µs in Node.js).\n * It is monotonically increasing within a browsing context, immune to wall-clock\n * adjustments that would cause `Date.now()` to produce negative deltas.\n *\n * @param currentZScore Trajectory Z-score from `SignalEngine.evaluateTrajectory`.\n * Pass 0 on cold start or when no baseline is configured.\n * Values < 0 are treated as 0 (no friction for healthy paths).\n * @returns Propensity score in [0, 1]. 0 when no baseline is available.\n */\n public getRealTimePropensity(currentZScore: number): number {\n // ── Throttle gate ──────────────────────────────────────────────────────────\n // performance.now() returns a DOMHighResTimeStamp in milliseconds.\n // Comparing against THROTTLE_MS determines whether we are within the\n // stable window established by the previous full computation.\n const now = performance.now();\n\n if (now - this.lastCalculationTime < this.THROTTLE_MS) {\n // Within the throttle window: return the last score with zero work.\n // This is the overwhelmingly common case during rapid `track()` bursts.\n return this.lastPropensity;\n }\n\n // ── No-baseline early exit ─────────────────────────────────────────────────\n // cachedBaseline === 0 means the target state is unreachable from the\n // current position within maxDepth hops (or updateBaseline has not been\n // called yet). Multiplying zero by any penalty factor yields zero;\n // we skip Math.exp() and return early.\n //\n // We still advance lastCalculationTime so repeated calls on an unreachable\n // target are throttled and do not cause a performance.now() call storm.\n if (this.cachedBaseline <= 0) {\n this.lastCalculationTime = now;\n this.lastPropensity = 0;\n return 0;\n }\n\n // ── Welford Z-score exponential friction penalty ───────────────────────────\n //\n // The Z-score is produced by SignalEngine.evaluateTrajectory using Welford's\n // online algorithm for running mean and variance of the log-likelihood series:\n //\n // z = (LL_observed − μ_baseline) / (σ_baseline × √(W_max / N))\n //\n // where LL_observed is the log-likelihood of the current trajectory under the\n // live graph, μ_baseline and σ_baseline are the Welford-derived mean and\n // standard deviation of the baseline log-likelihood distribution, W_max is the\n // maximum trajectory window length, and N is the current window length.\n //\n // We map this Z-score to a friction multiplier via exponential decay:\n //\n // frictionPenalty = exp(−α × max(0, z))\n //\n // The `max(0, z)` clamp zeroes out any benefit from below-baseline deviation\n // (the user is navigating more efficiently than average — we do not reward\n // this, we simply report no friction).\n // Non-finite z-scores (NaN, ±Infinity) propagate through Math.exp to NaN or\n // produce degenerate results, corrupting lastPropensity.\n // Treat them as 0 (no friction): the caller provided no usable signal.\n const safeZ = Number.isFinite(currentZScore) ? currentZScore : 0;\n const frictionPenalty = Math.exp(-this.alpha * Math.max(0, safeZ));\n\n // ── Combined propensity ────────────────────────────────────────────────────\n //\n // propensity = P_reach(current → target, maxDepth) × exp(−α × max(0, z))\n //\n // This is the Hadamard product of the structural graph estimate and the\n // real-time behavioural signal. Both factors are in [0, 1], so the result\n // is guaranteed to remain in [0, 1] without a final clamp.\n const propensity = this.cachedBaseline * frictionPenalty;\n\n // ── Persist for throttle window ────────────────────────────────────────────\n this.lastPropensity = propensity;\n this.lastCalculationTime = now;\n\n return propensity;\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/**\n * IntentEngine — Layer 2 Microkernel\n * --------------------------------------------------------\n * The raw, platform-agnostic intent detection kernel.\n *\n * This class has ZERO hardcoded references to `window`, `document`, or\n * `localStorage`. Every platform concern is delegated to the four adapter\n * interfaces supplied in `IntentEngineConfig`:\n *\n * - IInputAdapter — push-based navigation events from the host domain\n * - ILifecycleAdapter — pause / resume / exit-intent lifecycle signals\n * - IStateModel — Markov graph + Bloom filter signal evaluation\n * - IPersistenceAdapter — key-value storage I/O\n *\n * Layer 3 (`IntentManager`) wraps this kernel and wires in browser-specific\n * implementations for environments that have a DOM. All existing\n * `IntentManager` behavior is preserved — this class does not replace it.\n */\n\nimport type {\n IntentEngineConfig,\n IInputAdapter,\n ILifecycleAdapter,\n IStateModel,\n IPersistenceAdapter,\n} from '../types/microkernel.js';\nimport type { IntentEventMap } from '../types/events.js';\nimport { EventEmitter } from './event-emitter.js';\nimport { normalizeRouteState } from '../utils/route-normalizer.js';\n\n/** Maximum trajectory window kept for signal evaluation. */\nconst TRAJECTORY_WINDOW = 20;\n\nexport class IntentEngine {\n private readonly emitter = new EventEmitter<IntentEventMap>();\n private readonly stateModel: IStateModel;\n private readonly persistence: IPersistenceAdapter;\n private readonly lifecycle: ILifecycleAdapter;\n private readonly input: IInputAdapter | undefined;\n private readonly storageKey: string;\n private readonly stateNormalizer: ((state: string) => string) | undefined;\n private readonly onError: ((error: { code: string; message: string }) => void) | undefined;\n\n /** Unsubscribe functions collected during construction, released in destroy(). */\n private readonly teardowns: Array<() => void> = [];\n\n /** Most recently tracked state. */\n private previousState: string | null = null;\n /** Rolling window of recently visited states for trajectory evaluation. */\n private recentTrajectory: string[] = [];\n\n constructor(config: IntentEngineConfig) {\n this.stateModel = config.stateModel;\n this.persistence = config.persistence;\n this.lifecycle = config.lifecycle;\n this.input = config.input;\n this.storageKey = config.storageKey ?? 'passive-intent-engine';\n this.stateNormalizer = config.stateNormalizer;\n this.onError = config.onError;\n\n // ── 1. Restore persisted model state ──────────────────────────────────────\n let raw: string | null = null;\n try {\n raw = this.persistence.load(this.storageKey);\n } catch (err) {\n this.onError?.({\n code: 'RESTORE_READ',\n message: `IntentEngine: failed to read persisted state: ${\n err instanceof Error ? err.message : String(err)\n }`,\n });\n }\n if (raw !== null) {\n try {\n this.stateModel.restore(raw);\n } catch (err) {\n this.onError?.({\n code: 'RESTORE_PARSE',\n message: `IntentEngine: failed to restore persisted state: ${\n err instanceof Error ? err.message : String(err)\n }`,\n });\n }\n }\n\n // ── 2. Subscribe to IInputAdapter (push-based navigation) ─────────────────\n if (this.input) {\n try {\n const unsubInput = this.input.subscribe((state) => this._processState(state));\n this.teardowns.push(unsubInput);\n } catch (err) {\n this.onError?.({\n code: 'ADAPTER_SETUP',\n message: `IntentEngine: input.subscribe() threw: ${\n err instanceof Error ? err.message : String(err)\n }`,\n });\n }\n }\n\n // ── 3. Wire ILifecycleAdapter ──────────────────────────────────────────────\n // Persist on pause so state survives app backgrounding / tab hide.\n try {\n this.teardowns.push(\n this.lifecycle.onPause(() => {\n this._persist();\n }),\n );\n } catch (err) {\n this.onError?.({\n code: 'ADAPTER_SETUP',\n message: `IntentEngine: lifecycle.onPause() threw: ${\n err instanceof Error ? err.message : String(err)\n }`,\n });\n }\n\n // Exit-intent: only fire when the graph has a likely continuation path.\n if (typeof this.lifecycle.onExitIntent === 'function') {\n try {\n this.teardowns.push(\n this.lifecycle.onExitIntent(() => {\n if (this.previousState === null) return;\n let candidates: { state: string; probability: number }[] = [];\n try {\n candidates = this.stateModel.getLikelyNext(this.previousState, 0.4);\n } catch (err) {\n this.onError?.({\n code: 'STATE_MODEL',\n message: `IntentEngine: stateModel.getLikelyNext() threw: ${\n err instanceof Error ? err.message : String(err)\n }`,\n });\n }\n if (candidates.length === 0) return;\n this.emitter.emit('exit_intent', {\n state: this.previousState,\n likelyNext: candidates[0].state,\n });\n }),\n );\n } catch (err) {\n this.onError?.({\n code: 'ADAPTER_SETUP',\n message: `IntentEngine: lifecycle.onExitIntent() threw: ${\n err instanceof Error ? err.message : String(err)\n }`,\n });\n }\n }\n }\n\n /* ------------------------------------------------------------------ */\n /* Public API */\n /* ------------------------------------------------------------------ */\n\n /**\n * Subscribe to an intent event.\n *\n * ```ts\n * const off = engine.on('high_entropy', ({ state, normalizedEntropy }) => {\n * console.log(`High entropy in state ${state}: ${normalizedEntropy}`);\n * });\n * // later…\n * off(); // unsubscribe\n * ```\n *\n * @returns An unsubscribe function.\n */\n on<K extends keyof IntentEventMap>(\n event: K,\n listener: (payload: IntentEventMap[K]) => void,\n ): () => void {\n return this.emitter.on(event, listener);\n }\n\n /**\n * Manually track a state transition.\n *\n * Use this when no `IInputAdapter` is provided, or to supplement automatic\n * navigation tracking with custom application events.\n *\n * The state is normalized via `normalizeRouteState()` before processing.\n *\n * ```ts\n * engine.track('/checkout/review');\n * ```\n */\n track(state: string): void {\n this._processState(state);\n }\n\n /**\n * Tear down the engine: flush pending state, unsubscribe all listeners,\n * and release adapter resources.\n *\n * Call this in SPA cleanup paths (React `useEffect` return, Vue\n * `onUnmounted`, Angular `ngOnDestroy`).\n */\n destroy(): void {\n this._persist();\n for (const teardown of this.teardowns) {\n try {\n teardown();\n } catch (err) {\n this.onError?.({\n code: 'ADAPTER_TEARDOWN',\n message: `IntentEngine: teardown threw: ${\n err instanceof Error ? err.message : String(err)\n }`,\n });\n }\n }\n try {\n this.lifecycle.destroy();\n } catch (err) {\n this.onError?.({\n code: 'ADAPTER_TEARDOWN',\n message: `IntentEngine: lifecycle.destroy() threw: ${\n err instanceof Error ? err.message : String(err)\n }`,\n });\n }\n if (this.input) {\n try {\n this.input.destroy();\n } catch (err) {\n this.onError?.({\n code: 'ADAPTER_TEARDOWN',\n message: `IntentEngine: input.destroy() threw: ${\n err instanceof Error ? err.message : String(err)\n }`,\n });\n }\n }\n this.emitter.removeAll();\n }\n\n /* ------------------------------------------------------------------ */\n /* Private pipeline */\n /* ------------------------------------------------------------------ */\n\n /**\n * Core processing pipeline. Called by both the `IInputAdapter` push path\n * and the manual `track()` call path.\n *\n * Steps:\n * 1. Normalize state label\n * 2. Update state model (markSeen + recordTransition)\n * 3. Evaluate entropy signal → emit `high_entropy` if triggered\n * 4. Evaluate trajectory signal → emit `trajectory_anomaly` if triggered\n * 5. Emit `state_change`\n * 6. Persist model state\n */\n private _processState(raw: string): void {\n // ── Normalize ────────────────────────────────────────────────────────────\n let state = normalizeRouteState(raw);\n\n if (this.stateNormalizer) {\n try {\n const normalized = this.stateNormalizer(state);\n if (typeof normalized !== 'string') {\n this.onError?.({\n code: 'VALIDATION',\n message: `IntentEngine.track(): stateNormalizer must return a string, got ${typeof normalized}`,\n });\n return;\n }\n // Empty string is a deliberate \"skip this state\" signal.\n if (normalized === '') return;\n state = normalized;\n } catch (err) {\n this.onError?.({\n code: 'VALIDATION',\n message: `IntentEngine.track(): stateNormalizer threw: ${\n err instanceof Error ? err.message : String(err)\n }`,\n });\n return;\n }\n }\n\n if (state === '') {\n this.onError?.({\n code: 'VALIDATION',\n message: 'IntentEngine.track(): state label must not be an empty string',\n });\n return;\n }\n\n const from = this.previousState;\n\n // ── Update state model ────────────────────────────────────────────────────\n // markSeen / recordTransition are state mutations — if either throws we abort\n // this track() call entirely. previousState has NOT been advanced yet so the\n // engine state remains consistent.\n try {\n this.stateModel.markSeen(state);\n } catch (err) {\n this.onError?.({\n code: 'STATE_MODEL',\n message: `IntentEngine: stateModel.markSeen() threw: ${\n err instanceof Error ? err.message : String(err)\n }`,\n });\n return;\n }\n if (from !== null) {\n try {\n this.stateModel.recordTransition(from, state);\n } catch (err) {\n this.onError?.({\n code: 'STATE_MODEL',\n message: `IntentEngine: stateModel.recordTransition() threw: ${\n err instanceof Error ? err.message : String(err)\n }`,\n });\n return;\n }\n }\n\n // Advance internal position before evaluation so signals see the new state.\n this.previousState = state;\n this.recentTrajectory.push(state);\n if (this.recentTrajectory.length > TRAJECTORY_WINDOW) this.recentTrajectory.shift();\n\n // ── Signal evaluation (transition-dependent) ─────────────────────────────\n // Evaluation methods are read-only — if they throw we skip that signal and\n // continue so state_change and persistence still fire.\n if (from !== null) {\n // Entropy signal\n let entropyResult = { entropy: 0, normalizedEntropy: 0, isHigh: false };\n try {\n entropyResult = this.stateModel.evaluateEntropy(state);\n } catch (err) {\n this.onError?.({\n code: 'STATE_MODEL',\n message: `IntentEngine: stateModel.evaluateEntropy() threw: ${\n err instanceof Error ? err.message : String(err)\n }`,\n });\n }\n if (entropyResult.isHigh) {\n this.emitter.emit('high_entropy', {\n state,\n entropy: entropyResult.entropy,\n normalizedEntropy: entropyResult.normalizedEntropy,\n });\n }\n\n // Trajectory anomaly signal\n let trajectoryResult = null;\n try {\n trajectoryResult = this.stateModel.evaluateTrajectory(from, state, this.recentTrajectory);\n } catch (err) {\n this.onError?.({\n code: 'STATE_MODEL',\n message: `IntentEngine: stateModel.evaluateTrajectory() threw: ${\n err instanceof Error ? err.message : String(err)\n }`,\n });\n }\n if (trajectoryResult !== null && trajectoryResult.isAnomalous) {\n const sampleSize = trajectoryResult.sampleSize;\n this.emitter.emit('trajectory_anomaly', {\n stateFrom: from,\n stateTo: state,\n realLogLikelihood: trajectoryResult.logLikelihood,\n expectedBaselineLogLikelihood: trajectoryResult.baselineLogLikelihood,\n zScore: trajectoryResult.zScore,\n sampleSize,\n confidence: sampleSize < 10 ? 'low' : sampleSize < 30 ? 'medium' : 'high',\n });\n }\n }\n\n // ── Emit state_change (always) ────────────────────────────────────────────\n this.emitter.emit('state_change', { from, to: state });\n\n // ── Persist ───────────────────────────────────────────────────────────────\n this._persist();\n }\n\n /** Serialize and save model state via IPersistenceAdapter. */\n private _persist(): void {\n try {\n this.persistence.save(this.storageKey, this.stateModel.serialize());\n } catch (err) {\n this.onError?.({\n code: 'STORAGE_WRITE',\n message: `IntentEngine: persistence.save() threw: ${\n err instanceof Error ? err.message : String(err)\n }`,\n });\n }\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/**\n * BrowserLifecycleAdapter — web plugin for ILifecycleAdapter\n * --------------------------------------------------------\n * Bridges the browser's Page Visibility API and DOM interaction events\n * into the microkernel's ILifecycleAdapter contract.\n *\n * This plugin re-exports and explicitly types the existing\n * BrowserLifecycleAdapter implementation from the core adapters module,\n * making the interface contract visible at the plugin layer.\n *\n * The concrete class satisfies ILifecycleAdapter structurally (same method\n * signatures), and the subclass declaration below makes that relationship\n * explicit so tooling and consumers can rely on it without inspection.\n *\n * Events wired:\n * - `document.visibilitychange` → onPause / onResume callbacks\n * - `mousemove`, `scroll`, `touchstart`, `keydown` → onInteraction callbacks\n * (throttled to ≤ 1 per 1 000 ms)\n * - `document.documentElement` mouseleave (clientY ≤ 0) → onExitIntent callbacks\n */\n\nimport { BrowserLifecycleAdapter as _Base } from '../../adapters.js';\nimport type { ILifecycleAdapter } from '../../types/microkernel.js';\n\n/**\n * Standard browser lifecycle adapter for the IntentEngine microkernel.\n *\n * Drop-in for any environment where `document` and `window` are available.\n * All DOM access is guarded with `typeof document !== 'undefined'` checks so\n * the module can be imported in SSR / Node.js without throwing.\n *\n * ```ts\n * import { BrowserLifecycleAdapter } from '@passiveintent/core/plugins/web';\n *\n * const engine = new IntentEngine({\n * lifecycle: new BrowserLifecycleAdapter(),\n * // …\n * });\n * ```\n */\nexport class BrowserLifecycleAdapter extends _Base implements ILifecycleAdapter {}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/**\n * MouseKinematicsAdapter — web plugin for IInputAdapter\n * --------------------------------------------------------\n * Converts browser navigation and pointer/scroll physics into the engine's\n * canonical state string stream.\n *\n * Two classes of state are emitted:\n *\n * 1. **Navigation states** — emitted on `popstate` / `hashchange` events\n * and on first `subscribe()` call. State label = `location.pathname`.\n *\n * 2. **Scroll-depth sub-states** — emitted when the user crosses a scroll\n * depth threshold within the current page. State label format:\n * `{pathname}@scroll.{percent}` (e.g. `/product/details@scroll.50`).\n * Thresholds: 25 %, 50 %, 75 %, 100 %.\n *\n * 3. **Mouse-velocity states** — emitted when the pointer velocity crosses\n * the boundary between \"scanning\" (fast) and \"focused\" (slow/stopped).\n * State labels: `{pathname}@velocity.scanning`, `{pathname}@velocity.focused`.\n * This gives the intent engine a signal for reading-depth vs. rapid browsing.\n *\n * All DOM access is guarded so the module is safe to import in SSR/Node.js\n * environments (subscribe will be a no-op and return a no-op unsubscriber).\n *\n * Usage:\n * ```ts\n * const engine = new IntentEngine({\n * input: new MouseKinematicsAdapter(),\n * // …\n * });\n * ```\n */\n\nimport type { IInputAdapter } from '../../types/microkernel.js';\n\n/** Scroll depth thresholds at which sub-states are emitted (percent). */\nconst SCROLL_THRESHOLDS = [25, 50, 75, 100] as const;\n\n/** Pointer velocity (px/ms) below which the user is considered \"focused\". */\nconst FOCUSED_VELOCITY_THRESHOLD = 0.3;\n\n/** Minimum interval between mousemove velocity samples (ms). */\nconst VELOCITY_SAMPLE_INTERVAL_MS = 200;\n\n/** Debounce delay for scroll depth evaluation (ms). */\nconst SCROLL_DEBOUNCE_MS = 150;\n\nexport class MouseKinematicsAdapter implements IInputAdapter {\n private callback: ((state: string) => void) | null = null;\n\n /** Registered DOM event removers — collected in subscribe(), drained in destroy(). */\n private readonly cleanups: Array<() => void> = [];\n\n /* ── Scroll tracking ─────────────────────────────────────────── */\n /** Last scroll threshold percent that was emitted. */\n private lastScrollPercent: number = -1;\n private scrollDebounceTimer: ReturnType<typeof setTimeout> | null = null;\n\n /* ── Mouse velocity tracking ─────────────────────────────────── */\n private lastMouseX = 0;\n private lastMouseY = 0;\n private lastMouseTime = 0;\n /** Last velocity zone emitted: 'scanning' | 'focused' | null (not yet emitted). */\n private lastVelocityZone: 'scanning' | 'focused' | null = null;\n\n /* ── Current page path ───────────────────────────────────────── */\n private currentPath = '';\n\n /* ================================================================= */\n /* IInputAdapter */\n /* ================================================================= */\n\n subscribe(onState: (state: string) => void): () => void {\n if (typeof window === 'undefined') {\n // SSR / non-browser: return a no-op unsubscriber.\n return () => {};\n }\n\n this.callback = onState;\n this.currentPath = window.location.pathname;\n this.lastScrollPercent = -1;\n this.lastVelocityZone = null;\n\n // ── Emit initial page state ──────────────────────────────────────\n // Deferred via queueMicrotask so callers can register engine.on() listeners\n // before the first state_change fires. The engine constructor calls\n // subscribe() synchronously, meaning any .on() registrations that happen\n // after createBrowserIntent() returns would otherwise miss this event.\n queueMicrotask(() => onState(this.currentPath));\n\n // ── Navigation events ────────────────────────────────────────────\n // Covers back/forward (popstate) and hash-based routing (hashchange).\n //\n // NOTE — push-state SPAs (React Router, Next.js App Router, Vue Router, …)\n // use history.pushState / history.replaceState, which do NOT fire popstate.\n // Monkeypatching those methods is intentionally avoided here: it produces\n // global side-effects that compose poorly when multiple adapters or routers\n // are present on the same page.\n //\n // For push-state SPAs, use one of:\n // 1. A custom IInputAdapter that calls `engine.track()` inside the\n // router's navigation hook (e.g. React Router `history.listen`,\n // Vue Router `router.afterEach`, Next.js `router.events.on`).\n // 2. The raw IntentEngine path: `new IntentEngine({ input: myAdapter })`.\n const onPopState = (): void => this.handleNavigation();\n const onHashChange = (): void => this.handleNavigation();\n\n window.addEventListener('popstate', onPopState);\n window.addEventListener('hashchange', onHashChange);\n this.cleanups.push(() => {\n window.removeEventListener('popstate', onPopState);\n window.removeEventListener('hashchange', onHashChange);\n });\n\n // ── Scroll depth events ──────────────────────────────────────────\n const onScroll = (): void => this.scheduleScrollEvaluation();\n\n window.addEventListener('scroll', onScroll, { passive: true });\n this.cleanups.push(() => window.removeEventListener('scroll', onScroll));\n\n // ── Mouse velocity events ────────────────────────────────────────\n const onMouseMove = (e: MouseEvent): void => this.sampleMouseVelocity(e);\n\n window.addEventListener('mousemove', onMouseMove, { passive: true });\n this.cleanups.push(() => window.removeEventListener('mousemove', onMouseMove));\n\n return () => this.teardown();\n }\n\n destroy(): void {\n this.teardown();\n }\n\n /* ================================================================= */\n /* Navigation */\n /* ================================================================= */\n\n private handleNavigation(): void {\n if (typeof window === 'undefined') return;\n\n this.currentPath = window.location.pathname;\n\n // Cancel any pending scroll debounce from the previous page.\n if (this.scrollDebounceTimer !== null) {\n clearTimeout(this.scrollDebounceTimer);\n this.scrollDebounceTimer = null;\n }\n\n // Reset sub-state tracking for the new page.\n this.lastScrollPercent = -1;\n this.lastVelocityZone = null;\n\n // Reset mouse velocity tracking to prevent stale velocity calculations.\n this.lastMouseTime = 0;\n this.lastMouseX = 0;\n this.lastMouseY = 0;\n\n this.emit(this.currentPath);\n }\n\n /* ================================================================= */\n /* Scroll depth */\n /* ================================================================= */\n\n private scheduleScrollEvaluation(): void {\n if (this.scrollDebounceTimer !== null) {\n clearTimeout(this.scrollDebounceTimer);\n }\n this.scrollDebounceTimer = setTimeout(() => {\n this.scrollDebounceTimer = null;\n this.evaluateScrollDepth();\n }, SCROLL_DEBOUNCE_MS);\n }\n\n private evaluateScrollDepth(): void {\n if (typeof window === 'undefined' || typeof document === 'undefined') return;\n\n const scrollY = window.scrollY;\n const docHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight;\n\n if (docHeight <= 0) return;\n\n const percent = Math.min(100, Math.round((scrollY / docHeight) * 100));\n\n // Find the highest threshold crossed.\n let crossed: number | null = null;\n for (const threshold of SCROLL_THRESHOLDS) {\n if (percent >= threshold && threshold > this.lastScrollPercent) {\n crossed = threshold;\n }\n }\n\n if (crossed !== null) {\n this.lastScrollPercent = crossed;\n this.emit(`${this.currentPath}@scroll.${crossed}`);\n }\n }\n\n /* ================================================================= */\n /* Mouse velocity */\n /* ================================================================= */\n\n private sampleMouseVelocity(e: MouseEvent): void {\n const now = typeof performance !== 'undefined' ? performance.now() : Date.now();\n\n if (this.lastMouseTime === 0) {\n // First sample — seed without emitting.\n this.lastMouseX = e.clientX;\n this.lastMouseY = e.clientY;\n this.lastMouseTime = now;\n return;\n }\n\n const dt = now - this.lastMouseTime;\n if (dt < VELOCITY_SAMPLE_INTERVAL_MS) return;\n\n const dx = e.clientX - this.lastMouseX;\n const dy = e.clientY - this.lastMouseY;\n const distance = Math.sqrt(dx * dx + dy * dy);\n const velocity = distance / dt; // px/ms\n\n this.lastMouseX = e.clientX;\n this.lastMouseY = e.clientY;\n this.lastMouseTime = now;\n\n const zone: 'scanning' | 'focused' =\n velocity >= FOCUSED_VELOCITY_THRESHOLD ? 'scanning' : 'focused';\n\n if (zone !== this.lastVelocityZone) {\n this.lastVelocityZone = zone;\n this.emit(`${this.currentPath}@velocity.${zone}`);\n }\n }\n\n /* ================================================================= */\n /* Internal helpers */\n /* ================================================================= */\n\n private emit(state: string): void {\n this.callback?.(state);\n }\n\n private teardown(): void {\n for (const cleanup of this.cleanups) cleanup();\n this.cleanups.length = 0;\n\n if (this.scrollDebounceTimer !== null) {\n clearTimeout(this.scrollDebounceTimer);\n this.scrollDebounceTimer = null;\n }\n\n this.callback = null;\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/**\n * ContinuousGraphModel — web plugin for IStateModel\n * --------------------------------------------------------\n * Implements IStateModel for standard web routing by composing the existing\n * `MarkovGraph` (transition modeling + signal evaluation) and `BloomFilter`\n * (state membership) primitives.\n *\n * Evaluation logic mirrors `SignalEngine` exactly — same constants, same\n * z-score formula, same calibration path — so ContinuousGraphModel + IntentEngine\n * produce numerically identical signals to IntentManager when configured\n * identically.\n *\n * Serialization format (JSON string):\n * ```json\n * { \"bloomBase64\": \"<base64>\", \"graphBinary\": \"<base64>\" }\n * ```\n * Matches the wire format used by `PersistenceCoordinator` / `SyncPersistStrategy`\n * so payloads are compatible with existing persisted data.\n *\n * Configuration:\n * ```ts\n * const model = new ContinuousGraphModel({\n * graph: { highEntropyThreshold: 0.75, divergenceThreshold: 3.5 },\n * bloom: { bitSize: 2048, hashCount: 4 },\n * baseline: exportedGraph, // from IntentManager.exportGraph()\n * });\n * ```\n */\n\nimport { MarkovGraph } from '../../core/markov.js';\nimport { BloomFilter } from '../../core/bloom.js';\nimport { uint8ToBase64, base64ToUint8 } from '../../persistence/codec.js';\nimport type { IStateModel, EntropyResult, TrajectoryResult } from '../../types/microkernel.js';\nimport type { MarkovGraphConfig, BloomFilterConfig } from '../../types/events.js';\nimport type { SerializedMarkovGraph } from '../../core/markov.js';\nimport {\n MIN_SAMPLE_TRANSITIONS,\n MIN_WINDOW_LENGTH,\n MAX_WINDOW_LENGTH,\n SMOOTHING_EPSILON,\n} from '../../engine/constants.js';\n\nexport interface ContinuousGraphModelConfig {\n /**\n * Markov graph tuning: entropy threshold, divergence threshold, smoothing, etc.\n * Defaults match IntentManager's defaults.\n */\n graph?: MarkovGraphConfig;\n /** Bloom filter sizing. Default: bitSize=2048, hashCount=4. */\n bloom?: BloomFilterConfig;\n /**\n * Optional pre-trained baseline graph (from `IntentManager.exportGraph()` or\n * `MarkovGraph.toJSON()`). Required for `trajectory_anomaly` detection — when\n * absent, `evaluateTrajectory()` always returns `null`.\n */\n baseline?: SerializedMarkovGraph;\n}\n\n/** Wire format stored by `serialize()` / parsed by `restore()`. */\ninterface PersistedPayload {\n bloomBase64: string;\n graphBinary: string;\n}\n\nexport class ContinuousGraphModel implements IStateModel {\n private graph: MarkovGraph;\n private bloom: BloomFilter;\n private readonly baseline: MarkovGraph | null;\n private readonly graphConfig: MarkovGraphConfig;\n private readonly bloomConfig: BloomFilterConfig;\n\n constructor(config: ContinuousGraphModelConfig = {}) {\n this.graphConfig = config.graph ?? {};\n this.bloomConfig = config.bloom ?? {};\n this.graph = new MarkovGraph(this.graphConfig);\n this.bloom = new BloomFilter(this.bloomConfig);\n this.baseline = config.baseline\n ? MarkovGraph.fromJSON(config.baseline, this.graphConfig)\n : null;\n }\n\n /* ================================================================= */\n /* IStateModel — membership */\n /* ================================================================= */\n\n markSeen(state: string): void {\n this.bloom.add(state);\n }\n\n hasSeen(state: string): boolean {\n return this.bloom.check(state);\n }\n\n /* ================================================================= */\n /* IStateModel — transitions */\n /* ================================================================= */\n\n recordTransition(from: string, to: string): void {\n this.graph.incrementTransition(from, to);\n }\n\n getLikelyNext(state: string, threshold: number): { state: string; probability: number }[] {\n return this.graph.getLikelyNextStates(state, threshold);\n }\n\n /* ================================================================= */\n /* IStateModel — signal evaluation */\n /* ================================================================= */\n\n /**\n * Evaluate whether the current state's outgoing entropy is anomalously high.\n *\n * Guards:\n * - Fewer than `MIN_SAMPLE_TRANSITIONS` outgoing edges → not enough data.\n *\n * Mirrors `SignalEngine.evaluateEntropy()`.\n */\n evaluateEntropy(state: string): EntropyResult {\n const NOT_HIGH: EntropyResult = { entropy: 0, normalizedEntropy: 0, isHigh: false };\n\n if (this.graph.rowTotal(state) < MIN_SAMPLE_TRANSITIONS) {\n return NOT_HIGH;\n }\n\n const entropy = this.graph.entropyForState(state);\n const normalizedEntropy = this.graph.normalizedEntropyForState(state);\n const isHigh = normalizedEntropy >= this.graph.highEntropyThreshold;\n\n return { entropy, normalizedEntropy, isHigh };\n }\n\n /**\n * Evaluate whether the `from → to` transition is anomalous relative to the\n * baseline distribution.\n *\n * Guards:\n * - No baseline configured → `null`.\n * - Trajectory shorter than `MIN_WINDOW_LENGTH` → `null` (warm-up phase).\n *\n * When `baselineMeanLL` + `baselineStdLL` are configured on the graph, uses\n * z-score comparison. Otherwise falls back to raw average LL threshold.\n *\n * Mirrors `SignalEngine.evaluateTrajectory()`.\n */\n evaluateTrajectory(\n from: string,\n _to: string,\n trajectory: readonly string[],\n ): TrajectoryResult | null {\n if (this.baseline === null) return null;\n if (trajectory.length < MIN_WINDOW_LENGTH) return null;\n\n const real = MarkovGraph.logLikelihoodTrajectory(this.graph, trajectory, SMOOTHING_EPSILON);\n const expected = MarkovGraph.logLikelihoodTrajectory(\n this.baseline,\n trajectory,\n SMOOTHING_EPSILON,\n );\n\n const N = Math.max(1, trajectory.length - 1);\n const expectedAvg = expected / N;\n const threshold = -Math.abs(this.graph.divergenceThreshold);\n\n const hasCalibratedBaseline =\n typeof this.graph.baselineMeanLL === 'number' &&\n typeof this.graph.baselineStdLL === 'number' &&\n Number.isFinite(this.graph.baselineMeanLL) &&\n Number.isFinite(this.graph.baselineStdLL) &&\n this.graph.baselineStdLL > 0;\n\n const adjustedStd = hasCalibratedBaseline\n ? this.graph.baselineStdLL! * Math.sqrt(MAX_WINDOW_LENGTH / N)\n : 0;\n\n const zScore = hasCalibratedBaseline\n ? (expectedAvg - this.graph.baselineMeanLL!) / adjustedStd\n : expectedAvg;\n\n const isAnomalous = hasCalibratedBaseline ? zScore <= threshold : expectedAvg <= threshold;\n\n return {\n zScore,\n isAnomalous,\n logLikelihood: real,\n baselineLogLikelihood: expected,\n sampleSize: this.graph.rowTotal(from),\n };\n }\n\n /* ================================================================= */\n /* IStateModel — serialization */\n /* ================================================================= */\n\n /**\n * Serialize the live Markov graph and Bloom filter to a JSON string.\n *\n * Wire format matches `SyncPersistStrategy` so payloads written by\n * `IntentManager` / `PersistenceCoordinator` can be loaded here, and\n * vice versa.\n */\n serialize(): string {\n this.graph.prune();\n const graphBinary = uint8ToBase64(this.graph.toBinary());\n const bloomBase64 = this.bloom.toBase64();\n const payload: PersistedPayload = { bloomBase64, graphBinary };\n return JSON.stringify(payload);\n }\n\n /**\n * Restore the Markov graph and Bloom filter from a previously serialized string.\n * Throws on parse failure so `IntentEngine` can surface it through `onError`.\n */\n restore(serialized: string): void {\n const payload = JSON.parse(serialized) as Partial<PersistedPayload>;\n\n if (payload.graphBinary) {\n this.graph = MarkovGraph.fromBinary(base64ToUint8(payload.graphBinary), this.graphConfig);\n }\n\n if (payload.bloomBase64) {\n this.bloom = BloomFilter.fromBase64(payload.bloomBase64, this.bloomConfig);\n }\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/**\n * LocalStorageAdapter — web plugin for IPersistenceAdapter\n * --------------------------------------------------------\n * Implements IPersistenceAdapter on top of the browser's `window.localStorage`.\n *\n * Every access is guarded with `typeof window === 'undefined'` checks so the\n * module can be imported in SSR / Node.js / Web Worker environments without\n * throwing. When `window.localStorage` is unavailable the adapter silently\n * degrades to a no-op (load returns `null`, save is skipped).\n *\n * Note: `localStorage` is synchronous. For async backends (React Native\n * AsyncStorage, Capacitor Preferences, IndexedDB) use `IntentManager.createAsync()`\n * with an `AsyncStorageAdapter` — that path remains in Layer 3 (IntentManager).\n *\n * Usage:\n * ```ts\n * import { LocalStorageAdapter } from '@passiveintent/core/plugins/web';\n *\n * const engine = new IntentEngine({\n * persistence: new LocalStorageAdapter(),\n * // …\n * });\n * ```\n */\n\nimport type { IPersistenceAdapter } from '../../types/microkernel.js';\n\nexport class LocalStorageAdapter implements IPersistenceAdapter {\n /**\n * Load a value from localStorage.\n * Returns `null` when the key is absent, when localStorage is unavailable\n * (SSR, incognito with storage blocked, sandboxed iframe), or when a\n * `SecurityError` is thrown.\n */\n load(key: string): string | null {\n try {\n if (typeof window === 'undefined' || !window.localStorage) return null;\n return window.localStorage.getItem(key);\n } catch {\n // SecurityError accessing window.localStorage on sandboxed/opaque origins,\n // or SecurityError / other errors from getItem.\n return null;\n }\n }\n\n /**\n * Save a value to localStorage.\n * Silently no-ops when localStorage is unavailable.\n *\n * Unlike `BrowserStorageAdapter.setItem`, this method also catches and\n * **swallows** `QuotaExceededError` so that a full storage partition does\n * not surface an uncaught exception. Higher-layer error handling\n * (IntentEngine's `onError` callback) is the right place to observe this\n * failure; the caller (`IntentEngine._persist()`) wraps this call in its\n * own try/catch and routes any thrown error through `onError`.\n */\n save(key: string, value: string): void {\n try {\n if (typeof window === 'undefined' || !window.localStorage) return;\n window.localStorage.setItem(key, value);\n } catch {\n // SecurityError (sandboxed/opaque origin) or QuotaExceededError — swallowed.\n }\n }\n}\n","/**\n * Copyright (c) 2026 Purushottam <purushottam@passiveintent.dev>\n *\n * This source code is licensed under the AGPL-3.0-only license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/**\n * Progressive Disclosure layer — createBrowserIntent\n * --------------------------------------------------------\n * The 90 % use-case entry point for standard web applications.\n *\n * This factory wires all standard web plugins (BrowserLifecycleAdapter,\n * MouseKinematicsAdapter, ContinuousGraphModel, LocalStorageAdapter) into a\n * raw IntentEngine and returns the fully operational instance.\n *\n * Callers see ONE config object with no adapter boilerplate. The dependency-\n * injection complexity that powers the microkernel's plug-and-play flexibility\n * is invisible here — it exists for the 10 % who need React Native, Electron,\n * or custom fintech/food-delivery adapters via `new IntentEngine({…})` directly.\n *\n * ```ts\n * // Standard web — one line:\n * const intent = createBrowserIntent({ storageKey: 'my-app' });\n *\n * intent.on('high_entropy', ({ state }) => showHelpWidget(state));\n * intent.on('exit_intent', ({ likelyNext }) => prefetch(likelyNext));\n *\n * // Manual tracking alongside automatic kinematics:\n * intent.track('/checkout/review');\n *\n * // SPA teardown:\n * intent.destroy();\n * ```\n *\n * Architecture layers\n * -------------------\n * Layer 1 — Core algorithms : MarkovGraph, BloomFilter (pure, no I/O)\n * Layer 2 — Microkernel : IntentEngine (adapter interfaces)\n * Layer 3 — Web factory ← YOU ARE HERE (progressive disclosure)\n * Layer 4 — Framework SDKs : usePassiveIntent (React hook) (wraps IntentManager)\n */\n\nimport { IntentEngine } from './engine/intent-engine.js';\nimport { BrowserLifecycleAdapter } from './plugins/web/BrowserLifecycleAdapter.js';\nimport { MouseKinematicsAdapter } from './plugins/web/MouseKinematicsAdapter.js';\nimport { ContinuousGraphModel } from './plugins/web/ContinuousGraphModel.js';\nimport { LocalStorageAdapter } from './plugins/web/LocalStorageAdapter.js';\nimport type { MarkovGraphConfig, BloomFilterConfig } from './types/events.js';\nimport type { SerializedMarkovGraph } from './core/markov.js';\n\n/* ------------------------------------------------------------------ */\n/* BrowserConfig */\n/* ------------------------------------------------------------------ */\n\n/**\n * User-facing configuration for `createBrowserIntent`.\n *\n * All fields are optional — `createBrowserIntent({})` is a valid, fully-\n * operational call that uses sensible defaults for every setting.\n */\nexport interface BrowserConfig {\n /**\n * localStorage key used to persist the Bloom filter and Markov graph\n * across sessions.\n *\n * Use a unique key per application to avoid collisions when multiple\n * PassiveIntent instances share the same origin.\n *\n * Default: `'passive-intent-engine'`\n */\n storageKey?: string;\n\n /**\n * Pre-trained baseline graph exported from a previous session via\n * `IntentManager.exportGraph()` or `MarkovGraph.toJSON()`.\n *\n * Required for `trajectory_anomaly` detection. Without a baseline,\n * the engine emits `high_entropy` and `exit_intent` events but silently\n * skips trajectory z-score comparisons.\n */\n baseline?: SerializedMarkovGraph;\n\n /**\n * Markov graph tuning parameters. Omit to accept production-tested defaults:\n * - `highEntropyThreshold`: 0.75 (normalized entropy above which `high_entropy` fires)\n * - `divergenceThreshold`: 3.5 (z-score magnitude for `trajectory_anomaly`)\n * - `smoothingAlpha`: 0.1 (Dirichlet regularization — prevents cold-start spikes)\n * - `maxStates`: 500 (LFU eviction cap)\n */\n graph?: MarkovGraphConfig;\n\n /**\n * Bloom filter sizing. Omit to accept defaults (`bitSize: 2048, hashCount: 4`).\n * Use `BloomFilter.computeOptimal(expectedRoutes, targetFPR)` to size precisely.\n */\n bloom?: BloomFilterConfig;\n\n /**\n * Optional custom state normalizer applied **after** the built-in\n * `normalizeRouteState()` (which strips query strings, hashes, trailing\n * slashes, UUIDs, and numeric IDs ≥ 4 digits).\n *\n * Use this to collapse dynamic slugs the built-in normalizer misses:\n * ```ts\n * stateNormalizer: (s) => s.replace(/^\\/blog\\/[^/]+$/, '/blog/:slug')\n * ```\n * Returning an empty string silently drops that `track()` call.\n */\n stateNormalizer?: (state: string) => string;\n\n /**\n * Non-fatal error callback. The engine never throws — storage errors,\n * quota exhaustion, parse failures, and invalid API calls are all routed\n * here so the host app can log or alert without a try/catch at every call\n * site.\n *\n * ```ts\n * onError: ({ code, message }) => Sentry.captureMessage(message, { tags: { code } })\n * ```\n */\n onError?: (error: { code: string; message: string }) => void;\n}\n\n/* ------------------------------------------------------------------ */\n/* Factory */\n/* ------------------------------------------------------------------ */\n\n/**\n * Create a fully wired `IntentEngine` for standard browser environments.\n *\n * Automatically instantiates and injects:\n * - `ContinuousGraphModel` — Markov graph + Bloom filter state model\n * - `LocalStorageAdapter` — `window.localStorage` persistence\n * - `BrowserLifecycleAdapter` — Page Visibility API lifecycle events\n * - `MouseKinematicsAdapter` — URL navigation + scroll-depth + mouse-velocity input\n *\n * @param config Optional tuning parameters. Every field has a sensible default.\n * @returns A live `IntentEngine` instance, ready to receive `on()` subscriptions\n * and `track()` calls. Call `destroy()` in SPA teardown paths.\n *\n * @example Minimal setup\n * ```ts\n * const intent = createBrowserIntent();\n * intent.on('high_entropy', ({ state, normalizedEntropy }) => {\n * console.log('Confused navigation detected:', state, normalizedEntropy);\n * });\n * ```\n *\n * @example With baseline for trajectory anomaly detection\n * ```ts\n * const intent = createBrowserIntent({\n * storageKey: 'acme-shop',\n * baseline: importedBaselineGraph,\n * onError: ({ code, message }) => logger.warn(code, message),\n * });\n * ```\n */\nexport function createBrowserIntent(config: BrowserConfig = {}): IntentEngine {\n const stateModel = new ContinuousGraphModel({\n graph: config.graph,\n bloom: config.bloom,\n baseline: config.baseline,\n });\n\n const persistence = new LocalStorageAdapter();\n const lifecycle = new BrowserLifecycleAdapter();\n const input = new MouseKinematicsAdapter();\n\n return new IntentEngine({\n stateModel,\n persistence,\n lifecycle,\n input,\n storageKey: config.storageKey,\n stateNormalizer: config.stateNormalizer,\n onError: config.onError,\n });\n}\n"],"mappings":"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","names":["index_exports","__export","ATTENTION_RETURN_THRESHOLD_MS","AnomalyDispatcher","BenchmarkRecorder","BloomFilter","BroadcastSync","BrowserLifecycleAdapter","BrowserStorageAdapter","BrowserTimerAdapter","DriftProtectionPolicy","EventEmitter","IDLE_CHECK_INTERVAL_MS","IntentEngine","IntentManager","MAX_PLAUSIBLE_DWELL_MS","MAX_STATE_LENGTH","MarkovGraph","MemoryStorageAdapter","PropensityCalculator","SignalEngine","USER_IDLE_THRESHOLD_MS","computeBloomConfig","createBrowserIntent","normalizeRouteState","__toCommonJS","uint8ToBase64","bytes","parts","i","slice","base64ToUint8","base64","binary","arr","fnv1a","input","seed","hash","i","_mc","v","_scratchHashes","BloomFilter","_BloomFilter","config","existingBits","byteSize","item","h1","h2","index","expectedItems","targetFPR","ln2","ln2Sq","m","k","insertedItemsCount","exponent","bitZeroProbability","uint8ToBase64","base64","base64ToUint8","bitIndex","byteIndex","mask","computeBloomConfig","falsePositiveRate","estimatedFpRate","MarkovGraph","_MarkovGraph","config","rawSmoothingAlpha","state","existing","index","fromState","toState","from","to","row","nextCount","count","entropy","p","k","denominator","numUnobserved","pUnobserved","supportSize","maxEntropy","normalized","baseline","sequence","epsilon","sum","i","minProbability","fromIndex","results","toIndex","probability","label","b","total","liveCount","ranked","idx","a","evictTarget","evictSet","removedTotal","deadIdx","rows","edges","data","graph","tombstoneSet","r","e","encoder","totalSize","encodedLabels","buffer","view","offset","encoded","decoder","version","numStates","rawLabels","strLen","numFreed","numRows","numEdges","toCounts","createAccumulator","recordSample","acc","elapsedMs","maxSamples","percentile","sorted","p","idx","toOperationStats","a","b","textEncoder","BenchmarkRecorder","config","operation","startedAt","memoryFootprint","payload","asText","BrowserStorageAdapter","key","value","BrowserTimerAdapter","fn","delay","id","MemoryStorageAdapter","_BrowserLifecycleAdapter","cb","callback","idx","now","opts","evt","root","e","BrowserLifecycleAdapter","UUID_V4_RE","MONGO_ID_RE","NUMERIC_ID_RE","normalizeRouteState","url","qIdx","path","hIdx","MAX_PLAUSIBLE_DWELL_MS","ATTENTION_RETURN_THRESHOLD_MS","USER_IDLE_THRESHOLD_MS","IDLE_CHECK_INTERVAL_MS","fprToZScore","fpr","p","t","buildIntentManagerOptions","config","botProtection","dwellTimeEnabled","crossTabSync","rawHoldoutPct","holdoutPercent","graphFPR","graphConfig","configuredSmoothing","trajectorySmoothingEpsilon","storageKey","rawPersistDebounce","persistDebounceMs","rawPersistThrottle","persistThrottleMs","rawEventCooldown","eventCooldownMs","rawDwellMinSamples","dwellTimeMinSamples","dwellFPR","rawDwellZScore","dwellTimeZScoreThreshold","enableBigrams","rawBigramThreshold","bigramFrequencyThreshold","rawDriftMaxRate","driftMaxAnomalyRate","rawDriftWindowMs","driftEvaluationWindowMs","rawHesitationMs","hesitationCorrelationWindowMs","EventEmitter","event","listener","set","payload","EntropyGuard","timestamp","previous","count","windowBotScore","deltas","oldestIndex","i","currIdx","nextIdx","delta","mean","variance","d","updateDwellStats","current","dwellMs","previousCount","previousMean","previousM2","count","delta","meanMs","delta2","m2","dwellStd","stats","assertNever","x","AnomalyDispatcher","config","decision","now","getConfidence","sampleSize","SignalEngine","config","EntropyGuard","AnomalyDispatcher","now","_from","_to","_trajectory","decision","state","start","entropy","normalizedEntropy","from","to","trajectory","real","MarkovGraph","expected","N","expectedAvg","threshold","hasCalibratedBaseline","adjustedStd","zScore","shouldEmit","dwellMs","updated","updateDwellStats","std","dwellStd","BasePersistStrategy","ctx","data","graph","bloom","graphBinary","graphBytes","uint8ToBase64","err","throttleMs","elapsed","remainingMs","SyncPersistStrategy","payload","isQuota","AsyncPersistStrategy","asyncStorage","setItemPromise","PersistenceCoordinator","config","AsyncPersistStrategy","SyncPersistStrategy","health","code","message","err","graph","bloom","graphConfig","raw","parsed","bytes","base64ToUint8","MarkovGraph","BloomFilter","payloadByteLength","LifecycleCoordinator","config","BrowserLifecycleAdapter","adapter","hiddenDuration","currentState","owns","armIdleTimer","unsub","idleMs","DwellTimePolicy","config","ctx","dwellMs","BigramPolicy","graph","bigramFrequencyThreshold","from","to","trajectory","bigramFrom","bigramTo","DriftProtectionPolicy","maxAnomalyRate","evaluationWindowMs","now","MAX_STATE_LENGTH","isValidTransitionMessage","msg","isValidCounterMessage","BroadcastSync","channelName","graph","bloom","counters","event","from","to","key","by","current","data","CrossTabSyncPolicy","config","BroadcastSync","from","to","key","by","IntentManager","_IntentManager","config","EventEmitter","ctx","bloomAddStart","i","incrementStart","opts","buildIntentManagerOptions","BenchmarkRecorder","BrowserTimerAdapter","persistenceCoordinator","PersistenceCoordinator","BrowserStorageAdapter","restored","BloomFilter","MarkovGraph","driftPolicy","DriftProtectionPolicy","policies","SignalEngine","DwellTimePolicy","state","dwellMs","BigramPolicy","CrossTabSyncPolicy","LifecycleCoordinator","delta","candidates","fn","err","event","listener","storageKey","raw","preloadBridge","_omit","restConfig","normalizeRouteState","normalized","coerced","now","trackStart","start","seen","threshold","sanitize","payload","key","by","next","serialized","PropensityCalculator","alpha","throttleMs","graph","currentState","targetState","maxDepth","safeMaxDepth","queue","accumulated","node","neighbours","nextState","edgeProb","reachProb","nextPathVisited","currentZScore","now","safeZ","frictionPenalty","propensity","TRAJECTORY_WINDOW","IntentEngine","config","EventEmitter","raw","err","unsubInput","state","candidates","event","listener","teardown","normalizeRouteState","normalized","from","entropyResult","trajectoryResult","sampleSize","BrowserLifecycleAdapter","SCROLL_THRESHOLDS","FOCUSED_VELOCITY_THRESHOLD","VELOCITY_SAMPLE_INTERVAL_MS","SCROLL_DEBOUNCE_MS","MouseKinematicsAdapter","onState","onPopState","onHashChange","onScroll","onMouseMove","e","scrollY","docHeight","percent","crossed","threshold","now","dt","dx","dy","velocity","zone","state","cleanup","ContinuousGraphModel","config","MarkovGraph","BloomFilter","state","from","to","threshold","NOT_HIGH","entropy","normalizedEntropy","isHigh","_to","trajectory","real","expected","N","expectedAvg","hasCalibratedBaseline","adjustedStd","zScore","isAnomalous","graphBinary","uint8ToBase64","payload","serialized","base64ToUint8","LocalStorageAdapter","key","value","createBrowserIntent","config","stateModel","ContinuousGraphModel","persistence","LocalStorageAdapter","lifecycle","BrowserLifecycleAdapter","input","MouseKinematicsAdapter","IntentEngine"]}
|