La répétition espacée : ce que l'algorithme de Leitner apprend sur la structure des données

Kortex pour Kotlin, Kafkex pour Kafka, Vocably pour l'anglais, Lingopack pour le chinois : quatre applications de contenu différent, mais un seul moteur sous le capot, celui de la répétition espacée façon Leitner. Ce qui rend ce problème intéressant, ce n'est pas le contenu pédagogique, c'est la structure de données qu'il impose.
Le principe, en une phrase
Chaque carte appartient à une boîte (souvent numérotée de 1 à 5 ou 7). Une réponse correcte fait avancer la carte d'une boîte ; une réponse incorrecte la renvoie à la boîte 1. Plus une boîte est avancée, plus l'intervalle avant la prochaine révision est long. Le système exploite l'oubli progressif plutôt que de le combattre : on ne revoit une notion que juste avant qu'on soit sur le point de l'oublier.
Boîte 1 Boîte 2 Boîte 3 Boîte 4 Boîte 5
(1 jour) ──▶ (3 jours) ──▶ (7 jours) ──▶ (14 jours) ──▶ (30 jours)
▲ │ │ │ │
└───────────────┴───────────────┴───────────────┴───────────────┘
une réponse incorrecte renvoie toujours à la boîte 1
Plus une carte progresse vers la droite, plus l'intervalle avant sa prochaine révision s'allonge. Une seule erreur suffit à la faire repartir de zéro : c'est ce mécanisme, pas le contenu, qui fait tout le travail.
Pourquoi ce n'est pas un simple compteur
La tentation, en modélisant ça naïvement, est de stocker juste un numéro de boîte par carte. Le problème apparaît dès qu'on veut répondre à des questions produit simples : combien de cartes sont dues aujourd'hui ? quelle est la progression réelle d'un utilisateur sur un thème donné ? comment gérer plusieurs decks avec des rythmes de révision différents sans tout recalculer à chaque affichage ?
La structure qui fonctionne bien est un événement daté par carte (dernière révision, résultat, prochaine échéance calculée), plutôt qu'un simple état mutable. Ça transforme un problème de mise à jour en place en un problème de requête sur une timeline, beaucoup plus facile à faire évoluer : ajouter des statistiques de progression, ou changer l'algorithme de calcul d'intervalle, ne demande pas de migrer les données existantes.
Concrètement, ça donne une structure proche de ceci, plutôt qu'un simple
box: number :
type ReviewEvent = {
cardId: string;
reviewedAt: Date;
result: "correct" | "incorrect";
boxAfter: number;
dueAt: Date; // calculé à partir de boxAfter
};
Avec cette forme, "combien de cartes sont dues aujourd'hui" devient une
simple requête (dueAt <= now), et changer la durée des intervalles ne
demande de modifier que la fonction de calcul, jamais les données déjà
enregistrées.
Compteur naïf vs événement daté, ce que chaque question coûte
| Question produit | Avec box: number | Avec un événement daté par révision |
|---|---|---|
| Combien de cartes sont dues aujourd'hui ? | Recalcul complet à chaque affichage | Requête directe (dueAt <= now) |
| Quelle est la progression réelle sur un thème ? | Nécessite de recompter à chaque fois | Lecture de l'historique déjà stocké |
| Changer la durée des intervalles | Migration de toutes les cartes existantes | Modifier uniquement la fonction de calcul |
| Ajouter des statistiques de progression | Aucune donnée historique disponible | Déjà présent dans l'historique d'événements |
La dernière ligne est celle qui justifie le choix a posteriori : une statistique qu'on n'a pas pensé à exposer au premier jour reste possible avec un historique d'événements, alors qu'elle est irrécupérable avec un simple compteur qui écrase son état précédent à chaque révision.
Le vrai défi : la gamification sans sur-justification
Sur les applications destinées à mes enfants (dans la même famille de produits), la contrainte supplémentaire est de motiver sans récompense réelle : pas de points échangeables, pas de badge qui vaut quelque chose en dehors de l'app. La répétition espacée aide ici indirectement : voir une carte qu'on pensait avoir oubliée réussie du premier coup est une récompense intrinsèque suffisante, si l'algorithme est bien calibré pour présenter les cartes au bon moment. Un intervalle mal calculé (trop court, trop long) ruine cet effet et transforme l'app en corvée.
Ce qui se généralise d'une app à l'autre
Le moteur de révision de Kortex, une fois extrait de son contenu Kotlin spécifique, s'est révélé directement réutilisable pour Kafkex, puis pour Vocably et Lingopack. Ce qui change d'une app à l'autre, c'est uniquement le contenu (les cartes) et l'habillage (langue, thème visuel) : la logique de progression, elle, est un problème résolu une fois pour toutes. C'est un bon rappel que la vraie complexité d'un produit n'est pas toujours là où on l'attend : ici, ce n'est pas le contenu pédagogique qui était difficile, c'était la structure de données sous-jacente.