Aller au contenu

Moteur de règles

Le moteur de règles est le coeur d’Presage. Il évalue des conditions déclaratives par rapport au UserContext courant et retourne l’action appropriée.

Chaque règle cible un point d’adaptation spécifique (identifié par adaptationId). Quand un composant résout ce point d’adaptation, le moteur :

  1. Filtre les règles par adaptationId
  2. Trie par priority (la plus haute d’abord)
  3. Evalue les conditions de chaque règle par rapport au UserContext
  4. Retourne l’action de la première règle correspondante
  5. Retourne null si aucune règle ne correspond (le composant utilise sa variante par défaut)
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' },
}
OpérateurDescriptionExemple
eqEgalité stricte (===){ field: 'traits.role', operator: 'eq', value: 'admin' }
neqInégalité stricte (!==){ field: 'traits.plan', operator: 'neq', value: 'free' }

Tous les opérateurs numériques requièrent que la valeur du champ et la valeur de comparaison soient des nombres.

OpérateurDescriptionExemple
gtSupérieur à{ field: 'signals.sessionCount', operator: 'gt', value: 5 }
gteSupérieur ou égal{ field: 'signals.totalEvents', operator: 'gte', value: 100 }
ltInférieur à{ field: 'signals.daysSinceSignup', operator: 'lt', value: 7 }
lteInférieur ou égal{ field: 'traits.companySize', operator: 'lte', value: 50 }
OpérateurDescriptionExemple
inLa valeur est dans le tableau fourni{ field: 'traits.plan', operator: 'in', value: ['pro', 'enterprise'] }
notInLa valeur n’est pas dans le tableau fourni{ field: 'traits.role', operator: 'notIn', value: ['viewer', 'guest'] }
OpérateurDescriptionExemple
containsLa chaîne contient la sous-chaîne, ou le tableau contient l’élément{ field: 'traits.company', operator: 'contains', value: 'Corp' }
notContainsInverse de contains{ field: 'traits.locale', operator: 'notContains', value: 'zh' }
OpérateurDescriptionExemple
existsLa valeur n’est ni undefined ni null{ field: 'traits.company', operator: 'exists', value: true }
notExistsLa valeur est undefined ou null{ field: 'traits.signupDate', operator: 'notExists', value: true }
OpérateurDescriptionExemple
betweenLe nombre est dans [min, max] (inclus){ field: 'signals.sessionCount', operator: 'between', value: [5, 20] }
OpérateurDescriptionExemple
matchesLa chaîne correspond à une expression régulière{ field: 'traits.company', operator: 'matches', value: '^Acme.*' }

Chaque condition du tableau doit correspondre :

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

Au moins une condition doit correspondre :

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

Inverse le résultat d’une condition ou d’un groupe :

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

Les groupes peuvent être imbriqués pour exprimer une logique complexe :

// (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 },
],
},
],
}

Les règles sont évaluées de la priorité la plus haute à la plus basse. La première correspondance l’emporte.

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' },
},
]

Les champs utilisent la notation pointée pour accéder aux valeurs imbriquées dans le UserContext :

CheminRésout vers
maturitycontext.maturity (une chaîne)
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']

Sélectionne une variante spécifique à rendre :

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

Masque entièrement le point d’adaptation :

action: { type: 'hide' }

Réordonne les éléments (utilisé avec AdaptiveNav) :

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

Passe des props arbitraires au composant correspondant :

action: { type: 'modify', props: { showBetaBadge: true, maxItems: 10 } }
  1. Nommez les règles de manière descriptivenew-user-guided-tour est plus clair que rule-1
  2. Utilisez des écarts de priorité — Laissez de la marge entre les priorités (10, 20, 30) pour pouvoir insérer des règles plus tard sans renuméroter
  3. Gardez les conditions peu profondes — Les arbres booléens profondément imbriqués sont difficiles à déboguer ; préférez des structures plus plates
  4. Testez les règles en isolation — La classe RulesEngine peut être instanciée directement pour les tests unitaires
  5. Préférez in à plusieurs eq{ operator: 'in', value: ['pro', 'enterprise'] } est plus propre que deux conditions any