@passiveintent/core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../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"],"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 * 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 * 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 * 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 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 };\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 rawDwellZScore = config.dwellTime?.zScoreThreshold;\n const dwellTimeZScoreThreshold =\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/**\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 return {\n kind: 'trajectory_anomaly',\n payload: {\n stateFrom: from,\n stateTo: to,\n realLogLikelihood: real,\n expectedBaselineLogLikelihood: expected,\n zScore,\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 return {\n kind: 'dwell_time_anomaly',\n payload: {\n state,\n dwellMs,\n meanMs: updated.meanMs,\n stdMs: std,\n zScore,\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 IDLE_CHECK_INTERVAL_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 * - Tracks `lastInteractionAt` and polls every `IDLE_CHECK_INTERVAL_MS`\n * - Emits `user_idle` when inactivity exceeds `USER_IDLE_THRESHOLD_MS`\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 recurring idle-check timer.\n * Gracefully skips when the adapter does not implement `onInteraction`.\n */\n private startIdleTracking(adapter: LifecycleAdapter | null): void {\n if (!adapter || typeof adapter.onInteraction !== 'function') return;\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\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 this.scheduleIdleCheck();\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 /** Schedule the next idle-check tick using the TimerAdapter. */\n private scheduleIdleCheck(): void {\n this.idleCheckTimer = this.timer.setTimeout(() => {\n this.idleCheckTick();\n }, IDLE_CHECK_INTERVAL_MS);\n }\n\n /** Single idle-check iteration; re-arms the timer when done. */\n private idleCheckTick(): void {\n this.idleCheckTimer = null; // consumed\n\n const now = this.timer.now();\n if (\n !this.isIdle &&\n this.hasPreviousState() &&\n now - this.lastInteractionAt >= USER_IDLE_THRESHOLD_MS\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: now - this.idleStartedAt,\n });\n }\n }\n\n // Re-arm for next tick — but only when idle tracking is still active.\n // If destroy() was called from inside a user_idle / user_resumed handler\n // above, stopIdleTracking() will have nulled interactionUnsub; skipping\n // scheduleIdleCheck() here prevents a leaked timer after destruction.\n if (this.interactionUnsub !== null) {\n this.scheduleIdleCheck();\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"],"mappings":"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","names":["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","buildIntentManagerOptions","config","botProtection","dwellTimeEnabled","crossTabSync","rawHoldoutPct","holdoutPercent","graphConfig","configuredSmoothing","trajectorySmoothingEpsilon","storageKey","rawPersistDebounce","persistDebounceMs","rawPersistThrottle","persistThrottleMs","rawEventCooldown","eventCooldownMs","rawDwellMinSamples","dwellTimeMinSamples","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","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","unsub","idleMs","now","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"]}