Skip to content

@presage-kit/core

The core package provides the adaptive client, rules engine, tracker, persistence drivers, and all shared types.

Terminal window
pnpm add @presage-kit/core

Creates an AdaptiveClient instance — the central hub for rules, tracking, and context management.

import { createAdaptiveClient, createLocalStorageDriver } from '@presage-kit/core'
const client = createAdaptiveClient({
rules: [/* ... */],
tracker: {
adapters: [/* ... */],
builtIn: { maxEvents: 500 },
},
persistence: {
driver: createLocalStorageDriver('my-app'),
},
maturity: {
newMaxSessions: 5,
dormantDaysInactive: 21,
},
})
interface AdaptiveClientConfig {
rules?: AdaptationRule[]
tracker?: {
adapters?: TrackerAdapter[]
builtIn?: Partial<BuiltInTrackerConfig>
}
persistence?: {
driver: StorageDriver
}
maturity?: Partial<MaturityConfig>
}
FieldTypeDefaultDescription
rulesAdaptationRule[][]Initial set of adaptation rules
tracker.adaptersTrackerAdapter[][]External analytics adapters
tracker.builtIn.maxEventsnumber1000Maximum events in memory
tracker.builtIn.maxAgeMsnumber2592000000 (30 days)Max event age before pruning
persistence.driverStorageDrivernullStorage driver for traits and signals
maturityPartial<MaturityConfig>See defaultsMaturity threshold overrides
interface AdaptiveClient {
state: ReadableAtom<AdaptiveStoreState>
identify(userId: string, traits?: UserTraits): void
updateTraits(traits: Partial<UserTraits>): void
track(event: string, properties?: Record<string, unknown>): void
resolve(point: AdaptationPoint): ResolvedAdaptation
evaluateAction(adaptationId: string): AdaptationAction | null
getContext(): UserContext
destroy(): void
}

A reactive atom containing the current store state. Subscribe to it for change notifications.

const unsubscribe = client.state.subscribe((state) => {
console.log('Context changed:', state.context)
})

Associates the current session with a user. Merges provided traits with existing traits.

client.identify('user-123', {
role: 'admin',
plan: 'enterprise',
signupDate: '2025-01-15',
company: 'Acme Corp',
companySize: 150,
})

Also forwards the identify call to all registered tracker adapters.

Merges new traits into the existing traits without requiring a full identify() call.

client.updateTraits({ plan: 'enterprise', locale: 'fr' })

Records an event. Triggers debounced signal recomputation (100ms).

client.track('feature_used', { featureId: 'export' })
client.track('click', { elementId: 'sidebar-settings' })
client.track('custom_signal', { signalId: 'engagement', value: 3 })
client.track('page_view', { path: '/dashboard' })

Resolves an adaptation point by evaluating rules and returning the selected variant.

const result = client.resolve({
id: 'onboarding',
variants: ['guided-tour', 'standard', 'minimal'],
defaultVariant: 'standard',
strategy: { type: 'rules' },
})
console.log(result.selectedVariant) // 'guided-tour'
console.log(result.reason) // 'Rule "new-user-tour" matched'

Evaluates rules for an adaptation point and returns the raw action (or null if no rule matches). Useful for imperative control flow.

const action = client.evaluateAction('sidebar-nav')
if (action?.type === 'reorder') {
// Reorder navigation items
}

Returns the current UserContext snapshot.

const ctx = client.getContext()
console.log(ctx.traits.role) // 'admin'
console.log(ctx.signals.sessionCount) // 12
console.log(ctx.maturity) // 'active'

Tears down the client. Cancels pending timers and unsubscribes from state changes.

client.destroy()
interface AdaptiveStoreState {
context: UserContext
isReady: boolean
}
interface UserContext {
traits: UserTraits
signals: BehavioralSignals
maturity: Maturity
}
interface UserTraits {
userId?: string
role?: string
plan?: string
signupDate?: string
company?: string
companySize?: number
locale?: string
[key: string]: unknown
}

Extensible with any custom key-value pair.

interface BehavioralSignals {
sessionCount: number
totalEvents: number
featureUsage: Record<string, number>
lastSeenAt: string // ISO 8601 timestamp
firstSeenAt: string // ISO 8601 timestamp
currentSessionDuration: number
daysSinceSignup: number
clickMap: Record<string, number>
customSignals: Record<string, number>
}
type Maturity = 'new' | 'onboarding' | 'active' | 'power' | 'dormant'
interface MaturityConfig {
newMaxSessions: number // Default: 3
onboardingMaxSessions: number // Default: 10
powerMinFeatures: number // Default: 5
dormantDaysInactive: number // Default: 14
}
interface AdaptationRule {
id: string
adaptationId: string
priority: number
conditions: ConditionGroup
action: AdaptationAction
}
interface Condition {
field: string
operator: ConditionOperator
value: unknown
}
type ConditionOperator =
| 'eq' | 'neq'
| 'gt' | 'gte' | 'lt' | 'lte'
| 'in' | 'notIn'
| 'contains' | 'notContains'
| 'exists' | 'notExists'
| 'between'
| 'matches'
interface ConditionGroup {
all?: (Condition | ConditionGroup)[]
any?: (Condition | ConditionGroup)[]
not?: Condition | ConditionGroup
}
type AdaptationAction =
| { type: 'show'; variantId: string }
| { type: 'hide' }
| { type: 'reorder'; order: string[] }
| { type: 'modify'; props: Record<string, unknown> }
interface AdaptationPoint {
id: string
variants: readonly string[]
defaultVariant: string
strategy: Strategy
}
interface ResolvedAdaptation {
adaptationId: string
selectedVariant: string
strategy: Strategy['type'] // 'rules'
reason: string // e.g. 'Rule "my-rule" matched' or 'default'
resolvedAt: number // timestamp
}
type Strategy = { type: 'rules' }

Interface for plugging external analytics services into the tracker.

interface TrackerAdapter {
name: string
track(event: TrackerEvent): void | Promise<void>
identify(userId: string, traits: UserTraits): void | Promise<void>
flush?(): Promise<void>
}
interface TrackerEvent {
name: string
properties?: Record<string, unknown>
timestamp: number
userId?: string
sessionId: string
}
interface BuiltInTrackerConfig {
maxEvents: number // Default: 1000
maxAgeMs: number // Default: 2592000000 (30 days)
}

Interface for custom persistence drivers.

interface StorageDriver {
get<T>(key: string): T | null
set<T>(key: string, value: T): void
remove(key: string): void
clear(): void
}

Creates a StorageDriver backed by localStorage. Keys are stored as prefix:key.

import { createLocalStorageDriver } from '@presage-kit/core'
const driver = createLocalStorageDriver('my-app')
// Stores as 'my-app:traits', 'my-app:signals'

Gracefully returns null on get() when localStorage is unavailable (e.g., SSR).

Creates an in-memory StorageDriver. Data is lost when the process exits.

import { createMemoryDriver } from '@presage-kit/core'
const driver = createMemoryDriver()

The rules engine can be used directly for testing or advanced use cases.

import { RulesEngine } from '@presage-kit/core'
const engine = new RulesEngine()

Adds a single rule. Re-sorts rules by priority.

engine.addRule({
id: 'my-rule',
adaptationId: 'onboarding',
priority: 10,
conditions: { all: [{ field: 'maturity', operator: 'eq', value: 'new' }] },
action: { type: 'show', variantId: 'guided-tour' },
})

Adds multiple rules at once. Re-sorts once.

engine.addRules([rule1, rule2, rule3])

Removes a rule by its ID.

engine.removeRule('my-rule')

Evaluates all rules for the given adaptation ID against a UserContext. Returns the first matching action, or null.

const action = engine.evaluate('onboarding', {
traits: { role: 'admin' },
signals: { sessionCount: 2, totalEvents: 10, /* ... */ },
maturity: 'new',
})

Returns all registered rules as a readonly array.

const rules = engine.getRules() // readonly AdaptationRule[]

A minimal reactive primitive for advanced use cases. Follows the Svelte store contract.

import { atom } from '@presage-kit/core'
const count = atom(0)
// Read
console.log(count.get()) // 0
// Write
count.set(1)
// Subscribe (called immediately with current value)
const unsub = count.subscribe((value, oldValue) => {
console.log(`Changed from ${oldValue} to ${value}`)
})
// Listen (NOT called immediately)
const unlisten = count.listen((value, oldValue) => {
console.log(`Changed from ${oldValue} to ${value}`)
})
// Cleanup
unsub()
unlisten()
interface ReadableAtom<T> {
get(): T
subscribe(listener: Listener<T>): Unsubscribe
listen(listener: Listener<T>): Unsubscribe
}

Extends ReadableAtom with a set() method.

interface WritableAtom<T> extends ReadableAtom<T> {
set(value: T): void
}
type Listener<T> = (value: T, oldValue: T) => void
type Unsubscribe = () => void