Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions packages/core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,41 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

---

## [1.3.0] - 2026-03-29

### Added

- **`namespace` option for `BrowserStorageAdapter`** — the constructor now accepts an optional `namespace` string prefix (`default: 'passiveintent:'`) prepended to every `localStorage` key. Prevents key collisions when multiple PassiveIntent instances share the same origin (micro-frontend architectures).

```ts
// Each micro-frontend gets its own isolated key space
new BrowserStorageAdapter('checkout-mfe:');
new BrowserStorageAdapter('recommendations-mfe:');
```

- **`namespace` option for `LocalStorageAdapter`** — same isolation primitive on the `IntentEngine` (Layer 2) persistence adapter. Constructor: `new LocalStorageAdapter(namespace?)`. Defaults to `'passiveintent:'`. Pass `''` to disable prefixing entirely (not recommended when multiple instances share an origin).

- **`LocalStorageAdapter.delete(key)`** — new method on `IPersistenceAdapter` that removes a previously saved key from `localStorage`. Silently no-ops when `localStorage` is unavailable.

- **`namespace` field on `IntentManagerConfig`** — forwards a namespace prefix through `IntentManager` to `BrowserStorageAdapter`. Useful when constructing `IntentManager` directly rather than through adapters.

```ts
new IntentManager({ storageKey: 'intent', namespace: 'checkout-mfe:' });
// writes to localStorage key 'checkout-mfe:intent'
```

- **`namespace` field on `BrowserConfig`** — same forwarding for the `createBrowserIntent()` factory.

```ts
createBrowserIntent({ storageKey: 'intent', namespace: 'checkout-mfe:' });
```

### Changed

- **Internal re-export refactor** — `intent-sdk.ts` (internal barrel) was removed. All exports now resolve directly to their canonical source modules (`./engine/intent-manager.js`, `./core/bloom.js`, etc.). No public API change — all previously exported names remain available from `@passiveintent/core`.

---

## [1.2.2] - 2026-03-26

### Fixed
Expand Down
38 changes: 20 additions & 18 deletions packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,7 @@ All fields are optional. Pass them to `new IntentManager(config)` or `IntentMana
| Field | Type | Default | Description |
| ------------------------------- | -------------------------------------------------------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `storageKey` | `string` | `'passive-intent'` | `localStorage` key used to persist the Bloom filter and Markov graph. |
| `namespace` | `string` | `'passiveintent:'` | Prefix prepended to every `localStorage` key. Use distinct namespaces when multiple PassiveIntent instances share the same origin (micro-frontends). The full key becomes `"${namespace}${storageKey}"`. |
| `storage` | `StorageAdapter` | `BrowserStorageAdapter` | Synchronous storage backend. Override for custom persistence or tests. |
| `asyncStorage` | `AsyncStorageAdapter` | — | Async storage backend (React Native, IndexedDB, etc.). Use with `IntentManager.createAsync()`. Takes precedence over `storage` for writes. |
| `timer` | `TimerAdapter` | `BrowserTimerAdapter` | Timer backend. Override for deterministic tests. |
Expand Down Expand Up @@ -504,16 +505,16 @@ All fields are optional. Pass them to `new IntentManager(config)` or `IntentMana

### Adapters

| Export | Kind | Description |
| ------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `BrowserStorageAdapter` | class | Wraps `localStorage`. Use in any browser context. |
| `BrowserTimerAdapter` | class | Wraps `setTimeout` / `clearTimeout`. |
| `MemoryStorageAdapter` | class | In-memory fallback — no persistence. Useful for SSR, tests, or ephemeral sessions. |
| `BrowserLifecycleAdapter` | class | Page Visibility API adapter. Registers a `visibilitychange` listener and dispatches `onPause` / `onResume` callbacks. All `document` accesses are guarded so it is safe to import in SSR. |
| `StorageAdapter` | interface | Implement to provide a custom storage backend (IndexedDB, Capacitor Preferences, etc.). |
| `TimerAdapter` | interface | Implement to provide a custom timer backend (e.g. Node.js timers in tests). |
| `LifecycleAdapter` | interface | Implement to provide a custom page-visibility / app-lifecycle backend for React Native, Electron, or environments where `document` is unavailable. Pass via `IntentManagerConfig.lifecycleAdapter`. |
| `TimerHandle` | type | Opaque handle returned by `TimerAdapter.setTimeout`. |
| Export | Kind | Description |
| ------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `BrowserStorageAdapter` | class | Wraps `localStorage`. Constructor: `new BrowserStorageAdapter(namespace?)`. Optional `namespace` prefix (default `'passiveintent:'`) is prepended to every key — use distinct namespaces per micro-frontend to avoid key collisions. |
| `BrowserTimerAdapter` | class | Wraps `setTimeout` / `clearTimeout`. |
| `MemoryStorageAdapter` | class | In-memory fallback — no persistence. Useful for SSR, tests, or ephemeral sessions. |
| `BrowserLifecycleAdapter` | class | Page Visibility API adapter. Registers a `visibilitychange` listener and dispatches `onPause` / `onResume` callbacks. All `document` accesses are guarded so it is safe to import in SSR. |
| `StorageAdapter` | interface | Implement to provide a custom storage backend (IndexedDB, Capacitor Preferences, etc.). |
| `TimerAdapter` | interface | Implement to provide a custom timer backend (e.g. Node.js timers in tests). |
| `LifecycleAdapter` | interface | Implement to provide a custom page-visibility / app-lifecycle backend for React Native, Electron, or environments where `document` is unavailable. Pass via `IntentManagerConfig.lifecycleAdapter`. |
| `TimerHandle` | type | Opaque handle returned by `TimerAdapter.setTimeout`. |

### PropensityCalculator

Expand Down Expand Up @@ -659,14 +660,15 @@ Layer 4 — Framework SDKs usePassiveIntent (React hook) wraps IntentMana

### `createBrowserIntent(config?)` — Layer 3

| Field | Type | Default | Description |
| ----------------- | ------------------------------------------------ | ----------------------------- | ------------------------------------------------------------------------------ |
| `storageKey` | `string` | `'passive-intent-engine'` | `localStorage` key for cross-session persistence. |
| `baseline` | `SerializedMarkovGraph` | — | Pre-trained graph for `trajectory_anomaly` detection. |
| `graph` | `MarkovGraphConfig` | production defaults | Entropy / divergence thresholds, smoothing, state cap. |
| `bloom` | `BloomFilterConfig` | `bitSize: 2048, hashCount: 4` | Bloom filter sizing. |
| `stateNormalizer` | `(s: string) => string` | — | Custom normalizer applied after the built-in one. Return `''` to drop a state. |
| `onError` | `(e: { code: string; message: string }) => void` | — | Non-fatal error callback (storage errors, parse failures). |
| Field | Type | Default | Description |
| ----------------- | ------------------------------------------------ | ----------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `storageKey` | `string` | `'passive-intent-engine'` | `localStorage` key for cross-session persistence. |
| `namespace` | `string` | `'passiveintent:'` | Prefix prepended to every `localStorage` key. Use distinct values per micro-frontend to avoid key collisions. |
| `baseline` | `SerializedMarkovGraph` | — | Pre-trained graph for `trajectory_anomaly` detection. |
| `graph` | `MarkovGraphConfig` | production defaults | Entropy / divergence thresholds, smoothing, state cap. |
| `bloom` | `BloomFilterConfig` | `bitSize: 2048, hashCount: 4` | Bloom filter sizing. |
| `stateNormalizer` | `(s: string) => string` | — | Custom normalizer applied after the built-in one. Return `''` to drop a state. |
| `onError` | `(e: { code: string; message: string }) => void` | — | Non-fatal error callback (storage errors, parse failures). |

### `IntentEngine` — Layer 2

Expand Down
2 changes: 1 addition & 1 deletion packages/core/cypress/e2e/amazon.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ describe('Amazon Clone - Intent Engine Integration', () => {

// Verify localStorage was written
cy.window().then((win) => {
const payload = win.localStorage.getItem('amazon-intent-demo');
const payload = win.localStorage.getItem('passiveintent:amazon-intent-demo');
expect(payload).to.be.a('string');

const parsed = JSON.parse(payload as string);
Expand Down
4 changes: 2 additions & 2 deletions packages/core/cypress/e2e/browser-intent.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ describe('createBrowserIntent() — Layer 3 browser integration', () => {
beforeEach(() => {
cy.visit('/sandbox/browser-intent/index.html', {
onBeforeLoad: (win) => {
win.localStorage.removeItem('passive-intent-browser-test');
win.localStorage.removeItem('passiveintent:passive-intent-browser-test');
},
});

Expand Down Expand Up @@ -110,7 +110,7 @@ describe('createBrowserIntent() — Layer 3 browser integration', () => {
win.__engine.track('/checkout');
});
cy.window().then((win) => {
const stored = win.localStorage.getItem('passive-intent-browser-test');
const stored = win.localStorage.getItem('passiveintent:passive-intent-browser-test');
Comment thread
purushpsm147 marked this conversation as resolved.
expect(stored).to.not.be.null;
// Wire format: JSON with bloomBase64 + graphBinary keys
expect(stored).to.include('bloomBase64');
Expand Down
2 changes: 1 addition & 1 deletion packages/core/cypress/e2e/intent.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ describe('Privacy-First Intent Sandbox', () => {

cy.wait(600);
cy.window().then((win) => {
const payload = win.localStorage.getItem('passive-intent');
const payload = win.localStorage.getItem('passiveintent:passive-intent');
expect(payload, 'passive-intent should be written to localStorage').to.be.a('string');

const parsed = JSON.parse(payload as string);
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@passiveintent/core",
"version": "1.2.2",
"version": "1.3.0",
"description": "Privacy-first, SSR-safe intent detection engine using local Markov-chain inference and Bloom filters.",
"keywords": [
"intent",
Expand Down
2 changes: 1 addition & 1 deletion packages/core/sandbox/amazon/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* - Hesitation patterns (trajectory anomalies)
*/

import { IntentManager, SerializedMarkovGraph } from '../../src/intent-sdk.js';
import { IntentManager, SerializedMarkovGraph } from '../../src/index.js';

// ============================================
// BASELINE GRAPH
Expand Down
2 changes: 1 addition & 1 deletion packages/core/sandbox/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
BrowserLifecycleAdapter,
PropensityCalculator,
SerializedMarkovGraph,
} from '../src/intent-sdk.js';
} from '../src/index.js';

const baseline: SerializedMarkovGraph = {
states: ['/home', '/search', '/product', '/cart', '/checkout'],
Expand Down
2 changes: 1 addition & 1 deletion packages/core/scripts/perf-runner.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import fs from 'node:fs';
import path from 'node:path';
import { IntentManager } from '../dist/src/intent-sdk.js';
import { IntentManager } from '../dist/src/index.js';
import { printPerfSummary } from '../dist/src/reporting-utils.js';

class MemoryStorage {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/scripts/roc-experiment.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

import fs from 'node:fs';
import path from 'node:path';
import { IntentManager, MarkovGraph } from '../dist/src/intent-sdk.js';
import { IntentManager, MarkovGraph } from '../dist/src/index.js';

// ── polyfills ──────────────────────────────────────────────
class MemoryStorage {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/scripts/smoothing-alpha-benchmark.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { IntentManager, MarkovGraph } from '../dist/src/intent-sdk.js';
import { IntentManager, MarkovGraph } from '../dist/src/index.js';
import { MemoryStorage, setupTestEnvironment } from '../tests/helpers/test-env.mjs';

setupTestEnvironment();
Expand Down
40 changes: 38 additions & 2 deletions packages/core/src/adapters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,48 @@ export interface AsyncStorageAdapter {
* localStorage-backed adapter.
* Falls back to no-ops when `window` or `localStorage` is unavailable
* (e.g. SSR, Web Workers, or restrictive iframes).
*
* Accepts an optional `namespace` prefix that is prepended to every key
* before it reaches `localStorage`. Use this to isolate multiple
* PassiveIntent instances that share the same origin (micro-frontend
* collision prevention). Defaults to `'passiveintent:'`.
*
* **Legacy migration** — when using the default namespace, `getItem` first
* checks the namespaced key. If not found it falls back to the legacy
* unprefixed key. On a successful fallback the value is transparently
* migrated to the namespaced key and the old key is removed, so the
* one-time migration happens automatically on the first read after upgrade.
*/
export class BrowserStorageAdapter implements StorageAdapter {
private readonly namespace: string;

constructor(namespace = 'passiveintent:') {
this.namespace = namespace;
}

private nsKey(key: string): string {
return `${this.namespace}${key}`;
}

getItem(key: string): string | null {
if (typeof window === 'undefined' || !window.localStorage) return null;
try {
return window.localStorage.getItem(key);
const namespaced = window.localStorage.getItem(this.nsKey(key));
if (namespaced !== null) return namespaced;

// Legacy migration: when using the default namespace, check the old
// unprefixed key so existing installs are not silently wiped on upgrade.
if (this.namespace === 'passiveintent:') {
const legacy = window.localStorage.getItem(key);
if (legacy !== null) {
// Migrate to the namespaced key and remove the legacy entry.
window.localStorage.setItem(this.nsKey(key), legacy);
window.localStorage.removeItem(key);
return legacy;
}
}

return null;
} catch {
Comment thread
purushpsm147 marked this conversation as resolved.
// SecurityError in sandboxed iframes / opaque origins
return null;
Expand All @@ -63,7 +99,7 @@ export class BrowserStorageAdapter implements StorageAdapter {
// QuotaExceededError / SecurityError are intentionally NOT caught here.
// The caller (IntentManager.persist) wraps this in its own try/catch
// so the error surfaces through the configured onError callback.
window.localStorage.setItem(key, value);
window.localStorage.setItem(this.nsKey(key), value);
}
}

Expand Down
Loading
Loading