Ir al contenido

@presage-kit/core

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

Ventana de terminal
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()

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.

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:

ParamTypeDescription
itemsT[] (extends Orderable)Array of items to reorder
orderstring[]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.

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