Ir al contenido

Motor de reglas

El motor de reglas es el corazon de Presage. Evalua condiciones declarativas contra el UserContext actual y devuelve la accion apropiada.

Cada regla apunta a un adaptation point especifico (identificado por adaptationId). Cuando un componente resuelve ese adaptation point, el motor:

  1. Filtra las reglas por adaptationId
  2. Ordena por priority (la mas alta primero)
  3. Evalua las condiciones de cada regla contra el UserContext
  4. Devuelve la accion de la primera regla que coincida
  5. Devuelve null si ninguna regla coincide (el componente usa su variante por defecto)
const rule: AdaptationRule = {
id: 'enterprise-advanced-dashboard',
adaptationId: 'dashboard',
priority: 10,
conditions: {
all: [
{ field: 'traits.plan', operator: 'eq', value: 'enterprise' },
{ field: 'signals.sessionCount', operator: 'gte', value: 10 },
],
},
action: { type: 'show', variantId: 'advanced' },
}
OperadorDescripcionEjemplo
eqIgualdad estricta (===){ field: 'traits.role', operator: 'eq', value: 'admin' }
neqDesigualdad estricta (!==){ field: 'traits.plan', operator: 'neq', value: 'free' }

Todos los operadores numericos requieren que tanto el valor del campo como el valor de comparacion sean numeros.

OperadorDescripcionEjemplo
gtMayor que{ field: 'signals.sessionCount', operator: 'gt', value: 5 }
gteMayor o igual que{ field: 'signals.totalEvents', operator: 'gte', value: 100 }
ltMenor que{ field: 'signals.daysSinceSignup', operator: 'lt', value: 7 }
lteMenor o igual que{ field: 'traits.companySize', operator: 'lte', value: 50 }
OperadorDescripcionEjemplo
inEl valor esta en el array proporcionado{ field: 'traits.plan', operator: 'in', value: ['pro', 'enterprise'] }
notInEl valor no esta en el array proporcionado{ field: 'traits.role', operator: 'notIn', value: ['viewer', 'guest'] }
OperadorDescripcionEjemplo
containsLa cadena incluye la subcadena, o el array incluye el elemento{ field: 'traits.company', operator: 'contains', value: 'Corp' }
notContainsInverso de contains{ field: 'traits.locale', operator: 'notContains', value: 'zh' }
OperadorDescripcionEjemplo
existsEl valor no es undefined ni null{ field: 'traits.company', operator: 'exists', value: true }
notExistsEl valor es undefined o null{ field: 'traits.signupDate', operator: 'notExists', value: true }
OperadorDescripcionEjemplo
betweenEl numero esta dentro de [min, max] (inclusivo){ field: 'signals.sessionCount', operator: 'between', value: [5, 20] }
OperadorDescripcionEjemplo
matchesLa cadena coincide con una expresion regular{ field: 'traits.company', operator: 'matches', value: '^Acme.*' }

Todas las condiciones del array deben coincidir:

conditions: {
all: [
{ field: 'traits.role', operator: 'eq', value: 'admin' },
{ field: 'traits.plan', operator: 'eq', value: 'enterprise' },
{ field: 'signals.sessionCount', operator: 'gte', value: 5 },
],
}

Al menos una condicion debe coincidir:

conditions: {
any: [
{ field: 'maturity', operator: 'eq', value: 'new' },
{ field: 'maturity', operator: 'eq', value: 'onboarding' },
],
}

Invierte el resultado de una condicion o grupo:

conditions: {
not: { field: 'traits.plan', operator: 'eq', value: 'free' },
}

Los grupos pueden anidarse para expresar logica compleja:

// (admin AND enterprise) OR (power user with 50+ sessions)
conditions: {
any: [
{
all: [
{ field: 'traits.role', operator: 'eq', value: 'admin' },
{ field: 'traits.plan', operator: 'eq', value: 'enterprise' },
],
},
{
all: [
{ field: 'maturity', operator: 'eq', value: 'power' },
{ field: 'signals.sessionCount', operator: 'gte', value: 50 },
],
},
],
}

Las reglas se evaluan de mayor prioridad a menor. La primera coincidencia gana.

const rules = [
{
id: 'vip-override',
adaptationId: 'dashboard',
priority: 100, // Evaluated first
conditions: { all: [{ field: 'traits.role', operator: 'eq', value: 'vip' }] },
action: { type: 'show', variantId: 'vip-dashboard' },
},
{
id: 'enterprise-dashboard',
adaptationId: 'dashboard',
priority: 50, // Evaluated second
conditions: { all: [{ field: 'traits.plan', operator: 'eq', value: 'enterprise' }] },
action: { type: 'show', variantId: 'advanced' },
},
{
id: 'default-dashboard',
adaptationId: 'dashboard',
priority: 1, // Catch-all
conditions: {}, // Empty group matches everything
action: { type: 'show', variantId: 'standard' },
},
]

Los campos usan notacion de punto para acceder a valores anidados en el UserContext:

RutaSe resuelve a
maturitycontext.maturity (una cadena)
traits.rolecontext.traits.role
traits.companySizecontext.traits.companySize
signals.sessionCountcontext.signals.sessionCount
signals.featureUsage.exportcontext.signals.featureUsage.export
signals.clickMap.nav-settingscontext.signals.clickMap['nav-settings']

Selecciona una variante especifica para renderizar:

action: { type: 'show', variantId: 'guided-tour' }

Oculta el adaptation point completamente:

action: { type: 'hide' }

Reordena elementos (usado con AdaptiveNav):

action: { type: 'reorder', order: ['analytics', 'settings', 'home'] }

Pasa props arbitrarios al componente coincidente:

action: { type: 'modify', props: { showBetaBadge: true, maxItems: 10 } }
  1. Nombre las reglas de forma descriptivanew-user-guided-tour es mas claro que rule-1
  2. Use espacios entre prioridades — Deje espacio entre prioridades (10, 20, 30) para poder insertar reglas despues sin renumerar
  3. Mantenga las condiciones poco profundas — Los arboles booleanos profundamente anidados son dificiles de depurar; prefiera estructuras mas planas
  4. Pruebe las reglas de forma aislada — La clase RulesEngine puede instanciarse directamente para pruebas unitarias
  5. Prefiera in sobre multiples eq{ operator: 'in', value: ['pro', 'enterprise'] } es mas limpio que dos condiciones any