@presage-kit/core
The core package provides the adaptive client, rules engine, tracker, persistence drivers, and all shared types.
pnpm add @presage-kit/corecreateAdaptiveClient(config)
Section titled “createAdaptiveClient(config)”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, },})AdaptiveClientConfig
Section titled “AdaptiveClientConfig”interface AdaptiveClientConfig { rules?: AdaptationRule[] tracker?: { adapters?: TrackerAdapter[] builtIn?: Partial<BuiltInTrackerConfig> } persistence?: { driver: StorageDriver } maturity?: Partial<MaturityConfig>}| Field | Type | Default | Description |
|---|---|---|---|
rules | AdaptationRule[] | [] | Initial set of adaptation rules |
tracker.adapters | TrackerAdapter[] | [] | External analytics adapters |
tracker.builtIn.maxEvents | number | 1000 | Maximum events in memory |
tracker.builtIn.maxAgeMs | number | 2592000000 (30 days) | Max event age before pruning |
persistence.driver | StorageDriver | null | Storage driver for traits and signals |
maturity | Partial<MaturityConfig> | See defaults | Maturity threshold overrides |
AdaptiveClient
Section titled “AdaptiveClient”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)})identify(userId, traits?)
Section titled “identify(userId, traits?)”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.
updateTraits(traits)
Section titled “updateTraits(traits)”Merges new traits into the existing traits without requiring a full identify() call.
client.updateTraits({ plan: 'enterprise', locale: 'fr' })track(event, properties?)
Section titled “track(event, properties?)”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' })resolve(point)
Section titled “resolve(point)”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'evaluateAction(adaptationId)
Section titled “evaluateAction(adaptationId)”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}getContext()
Section titled “getContext()”Returns the current UserContext snapshot.
const ctx = client.getContext()console.log(ctx.traits.role) // 'admin'console.log(ctx.signals.sessionCount) // 12console.log(ctx.maturity) // 'active'destroy()
Section titled “destroy()”Tears down the client. Cancels pending timers and unsubscribes from state changes.
client.destroy()AdaptiveStoreState
Section titled “AdaptiveStoreState”interface AdaptiveStoreState { context: UserContext isReady: boolean}UserContext
Section titled “UserContext”interface UserContext { traits: UserTraits signals: BehavioralSignals maturity: Maturity}UserTraits
Section titled “UserTraits”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.
BehavioralSignals
Section titled “BehavioralSignals”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>}Maturity
Section titled “Maturity”type Maturity = 'new' | 'onboarding' | 'active' | 'power' | 'dormant'MaturityConfig
Section titled “MaturityConfig”interface MaturityConfig { newMaxSessions: number // Default: 3 onboardingMaxSessions: number // Default: 10 powerMinFeatures: number // Default: 5 dormantDaysInactive: number // Default: 14}AdaptationRule
Section titled “AdaptationRule”interface AdaptationRule { id: string adaptationId: string priority: number conditions: ConditionGroup action: AdaptationAction}Condition
Section titled “Condition”interface Condition { field: string operator: ConditionOperator value: unknown}ConditionOperator
Section titled “ConditionOperator”type ConditionOperator = | 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'notIn' | 'contains' | 'notContains' | 'exists' | 'notExists' | 'between' | 'matches'ConditionGroup
Section titled “ConditionGroup”interface ConditionGroup { all?: (Condition | ConditionGroup)[] any?: (Condition | ConditionGroup)[] not?: Condition | ConditionGroup}AdaptationAction
Section titled “AdaptationAction”type AdaptationAction = | { type: 'show'; variantId: string } | { type: 'hide' } | { type: 'reorder'; order: string[] } | { type: 'modify'; props: Record<string, unknown> }AdaptationPoint
Section titled “AdaptationPoint”interface AdaptationPoint { id: string variants: readonly string[] defaultVariant: string strategy: Strategy}ResolvedAdaptation
Section titled “ResolvedAdaptation”interface ResolvedAdaptation { adaptationId: string selectedVariant: string strategy: Strategy['type'] // 'rules' reason: string // e.g. 'Rule "my-rule" matched' or 'default' resolvedAt: number // timestamp}Strategy
Section titled “Strategy”type Strategy = { type: 'rules' }TrackerAdapter
Section titled “TrackerAdapter”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>}TrackerEvent
Section titled “TrackerEvent”interface TrackerEvent { name: string properties?: Record<string, unknown> timestamp: number userId?: string sessionId: string}BuiltInTrackerConfig
Section titled “BuiltInTrackerConfig”interface BuiltInTrackerConfig { maxEvents: number // Default: 1000 maxAgeMs: number // Default: 2592000000 (30 days)}StorageDriver
Section titled “StorageDriver”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}createLocalStorageDriver(prefix)
Section titled “createLocalStorageDriver(prefix)”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).
createMemoryDriver()
Section titled “createMemoryDriver()”Creates an in-memory StorageDriver. Data is lost when the process exits.
import { createMemoryDriver } from '@presage-kit/core'
const driver = createMemoryDriver()Orderable
Section titled “Orderable”Base interface for items that can be reordered by reorderItems() and the AdaptiveOrder/useAdaptiveOrder APIs. Every item must have a unique id.
interface Orderable { id: string [key: string]: unknown}The NavItem interface used by AdaptiveNav/useAdaptiveNav extends Orderable.
reorderItems(items, order)
Section titled “reorderItems(items, order)”A pure utility function that reorders items according to an order array. Items listed in order appear first (in that order), followed by any remaining items not mentioned in order, preserving their original relative order.
import { reorderItems } from '@presage-kit/core'
const items = [ { id: 'a', label: 'Alpha' }, { id: 'b', label: 'Beta' }, { id: 'c', label: 'Gamma' },]
const reordered = reorderItems(items, ['c', 'a'])// [{ id: 'c', label: 'Gamma' }, { id: 'a', label: 'Alpha' }, { id: 'b', label: 'Beta' }]Parameters:
| Param | Type | Description |
|---|---|---|
items | T[] (extends Orderable) | Array of items to reorder |
order | string[] | Array of id values specifying the desired order |
Return type: T[] — A new array with items reordered. The original array is not mutated.
This is the same reorder logic used internally by AdaptiveOrder, AdaptiveNav, and useAdaptiveOrder.
RulesEngine
Section titled “RulesEngine”The rules engine can be used directly for testing or advanced use cases.
import { RulesEngine } from '@presage-kit/core'
const engine = new RulesEngine()addRule(rule)
Section titled “addRule(rule)”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' },})addRules(rules)
Section titled “addRules(rules)”Adds multiple rules at once. Re-sorts once.
engine.addRules([rule1, rule2, rule3])removeRule(ruleId)
Section titled “removeRule(ruleId)”Removes a rule by its ID.
engine.removeRule('my-rule')evaluate(adaptationId, context)
Section titled “evaluate(adaptationId, context)”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',})getRules()
Section titled “getRules()”Returns all registered rules as a readonly array.
const rules = engine.getRules() // readonly AdaptationRule[]atom(initialValue)
Section titled “atom(initialValue)”A minimal reactive primitive for advanced use cases. Follows the Svelte store contract.
import { atom } from '@presage-kit/core'
const count = atom(0)
// Readconsole.log(count.get()) // 0
// Writecount.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}`)})
// Cleanupunsub()unlisten()ReadableAtom<T>
Section titled “ReadableAtom<T>”interface ReadableAtom<T> { get(): T subscribe(listener: Listener<T>): Unsubscribe listen(listener: Listener<T>): Unsubscribe}WritableAtom<T>
Section titled “WritableAtom<T>”Extends ReadableAtom with a set() method.
interface WritableAtom<T> extends ReadableAtom<T> { set(value: T): void}Listener<T>
Section titled “Listener<T>”type Listener<T> = (value: T, oldValue: T) => voidUnsubscribe
Section titled “Unsubscribe”type Unsubscribe = () => void