Doc / Technique / Moteur de session
Génération d'une session d'exercices.
La référence complète : le planificateur pur qui fixe l'ensemble d'exercices, les constantes de calibration exactes, le prompt qu'il assemble, et la boucle de validation qui l'impose. Vérifié contre le code du moteur : chaque symbole ci-dessous est un identifiant réel.
Source de référence : ignia_core/src/sessions/grid.rs, ignia_core/src/sessions/generate.rs, ignia_core/src/sessions/compose.rs, ignia_core/src/sessions/bloom.rs. Version narrative : sessions d'exercices.
Le pipeline.
Une génération de session, de bout en bout. Le planificateur est pur (ni base de données, ni lecture d'horloge), donc le plan est reproductible à partir de l'instantané d'état et de la graine de session.
Le planificateur tourne avant tout appel au fournisseur. Une grille dont tous les créneaux sont couverts par la banque de termes se termine sans aucun appel IA.
Les six étapes de grid().
Signature : grid(skind, &[LpState], base_lambda, n0, salt) -> Vec<Slot>. Renvoie vide pour les types de session que la grille ne possède pas (Recalibrate, Project, Freeform, Placement), qui retombent sur le chemin de prompt libre.
Structures de données.
Instantané d'entrée et créneau de sortie. Les deux dans sessions/grid.rs.
LpState, assemblé par build_lp_states()
| Champ | Type | Source |
|---|---|---|
| lp_id | String | identifiant de la notion |
| bloom | Bloom | Bloom::from_opt(lp.bloom_level). Null retombe sur Understand |
| mastery | f64 | lp.mastery |
| reps | i64 | lp.reps |
| lapses_since_success | i64 | lp.lapses_since_success |
| forecast_retention | f64 | mastery::forecast_retention(state, now) |
| is_due | bool | mastery::is_due(state, now) |
| band | MasteryLevel | mastery::level_of : New / Weak <0,5 / Developing <0,8 / Strong |
| misconception | bool | mastery::open_misconception_phrases, une seule analyse pour tout l'ensemble visé. La même analyse fournit les formulations enregistrées à append_grid_plan, de sorte que le drapeau et sa preuve ne peuvent pas diverger |
| transfer_passed | bool | lp.transfer_passed. Monotone, posé sur une sonde de transfert réussie |
Slot, un exercice que l'IA doit rédiger
| Champ | Signification | Atteint le prompt ? |
|---|---|---|
| lp_id | Notion visée ; résolue en son libellé pour le prompt | oui |
| kind | Type préféré de la grille ; toujours égal à allowed_kinds[0]. Aussi le type du chemin libre. | oui |
| allowed_kinds | Tous les types que l'IA peut choisir, tous dans le palier du créneau, filtrés par plancher et plafond. Longueur 1 = imposé. | oui |
| scaffold_level | faded ou solo : l'échelle de soutien. Un créneau faded porte une consigne de donner une partie du travail ; solo est le défaut et reste non marqué | oui |
| misconception_hint | true_false imposé dont l'option FAUSSE encode l'erreur enregistrée de l'apprenant, citée dans la consigne. data.rationale est obligatoire sur ce créneau et revérifié au retour | oui |
| transfer_probe | Sonde en contexte neuf imposée ; persistée dans questions.transfer_probe | oui |
Groupes, planchers et mélange de base.
Six paliers cognitifs forment l'unité de proportion. true_false est délibérément absent de toutes les listes de membres, car il est injecté orthogonalement et n'est jamais un type de quota.
| Palier | Groupe | Types membres (charge croissante) |
|---|---|---|
| 1 | Rappel | flashcard |
| 2 | Reconnaissance | matching · mcq |
| 3 | Rappel indicé | fill_blank · short_answer · word_order |
| 4 | Procédural | numeric_answer · fill_blank · solve_equation · code_trace |
| 5 | Analyse | categorize · ordering · code_bug_spot |
| 6 | Production | short_answer · code_bug_spot · writing |
base_mix(bloom) donne le pourcentage par palier, palier de maîtrise neutre, λ = 0. floor_group_tier(bloom) est le palier le plus bas portant de la masse pour « appliquer » et au-dessus ; en dessous, le mélange est mis à zéro puis renormalisé.
| Bloom | Plancher | Rappel | Recon. | Indicé | Procéd. | Analyse | Production |
|---|---|---|---|---|---|---|---|
| Remember | 1 | 40 | 25 | 35 | 0 | 0 | 0 |
| Understand | 3 | 5 | 20 | 50 | 5 | 20 | 0 |
| Apply | 4 | 0 | 0 | 15 | 65 | 15 | 5 |
| Analyze | 5 | 0 | 0 | 5 | 10 | 65 | 20 |
| Evaluate | 6 | 0 | 0 | 0 | 5 | 25 | 70 |
| Create | 6 | 0 | 0 | 0 | 5 | 20 | 75 |
Le plancher ne s'applique que pour bloom.tier() >= 3 ; pour remember et understand les paliers bas restent légaux. Un plancher par type distinct, bloom::kind_violates_floor, retire des types individuels de l'ensemble proposé (par exemple mcq et writing sont tous deux illégaux sur une notion Apply : l'un trop bas, l'autre trop haut).
Constantes de calibration.
Toutes réglables ; ce sont les valeurs v1 réellement compilées.
| Constante | Valeur | Rôle |
|---|---|---|
| W_LAPSE / W_MAST / W_NEW | 0,45 / 0,35 / 0,20 | Poids de difficulté : s = W_LAPSE·min(lapses/3,1) + W_MAST·(1−mastery) + W_NEW·[reps==0] |
| LAPSE_NORM | 3,0 | Nombre d'échecs à partir duquel le terme sature. Un échec est un véritable échec de rappel ; une réponse rappelée mais imparfaite n'en est pas un |
| STRUGGLE_HIGH / STRUGGLE_LOW | 0,6 / 0,3 | Levier de charge intra-palier : au-dessus → membre le moins chargé, en dessous → le plus chargé, entre → rotation |
| GS_HIGH / GS_LOAD | 0,5 / 0,8 | Difficulté moyenne à laquelle la session raccourcit, et le facteur appliqué. La moyenne ne porte que sur les notions déjà travaillées par le learner — une première rencontre se situe par construction en haut de l'échelle de difficulté, donc inclure les notions neuves faisait lire une session entièrement neuve comme une session en difficulté. Une session sans aucune notion déjà travaillée n'est pas traitée comme difficile |
| COUNT_STRUGGLE / COUNT_FADE | 2,0 / 1,0 | Poids de besoin par notion : 1 + 2·s + 1·is_due·(1−forecast_retention) |
| TILT_STEP | 8,0 | Points de pourcentage déplacés par unité de tilt de profondeur λ |
| MCQ_CAP | 0,30 | floor(0,30·n) créneaux peuvent se voir proposer mcq, à l'échelle de la session |
| MAX_INJECTS_PER_SESSION | 2 | Contrôles d'idée fausse ajoutés par session, classés par difficulté ressentie. Remplace l'ancien seuil MISC_THRESHOLD, qui était à l'envers : une idée fausse tenue avec aisance maintient la difficulté ressentie basse, donc le seuil ne s'ouvrait jamais sur le cas qui comptait |
| MAX_PROBES_PER_SESSION | 2 | Sondes de transfert ajoutées par session, même règle de sélection |
| MAX_ATTEMPTS | 3 | Cycles générer → valider → régénérer avant échec explicite |
| DEBT_SUSPEND_FACTOR | 2,0 | Arriéré (en multiples du budget) à partir duquel une session de cours abandonne tout contenu neuf |
| NEW_FLOOR_SHARE | 0,3 | Fraction du budget réservée au contenu neuf avant mise à l'échelle par l'appétit de nouveauté |
| DEFAULT_NOVELTY | 0,5 | Appétit de nouveauté de l'apprenant ; global pour l'instant, par apprenant plus tard |
target_exercise_count(kind, duration_min) = base × clamp(durée/30 ; 0,5 ; 2,0), arrondi, minimum 1.
| Type de session | Base à 30 min | Comportement propre à la grille |
|---|---|---|
| Course / Rewrite | 7 | Poids de besoin 1 + 0,25·(palier−1) ; aucune insertion (le premier enseignement n'est pas la frontière de maîtrise) |
| Practice / Review | 10 | Review applique λ − 0,5, un biais de récupération plutôt qu'un étirement |
| Exam | 8 | Aveugle à la maîtrise : besoins uniformes, tout en solo, aucune insertion, pas de prose d'adaptation, préférences de l'apprenant retirées |
| Transfer | 4 | λ = 2,0 imposé : application au palier le plus haut |
| Recalibrate | 5 | Hors grille : chemin de prompt distinct |
| Project / Freeform | 3 | Hors grille |
| Placement | 6 | Hors grille ; construit son propre compte depuis le prompt de profondeur |
depth_tilt(learning_depth) associe la chaîne de profondeur du brief à λ par correspondance de sous-chaîne :
| Profondeur | λ | Effet |
|---|---|---|
| surface | −1,0 | Draine les paliers hauts vers le plancher relâché |
| (non défini) | 0,0 | Mélange de base inchangé |
| deep (practical) | +1,0 | Draine les paliers bas vers le plus haut palier non vide |
| deep theoretical | +1,5 | Idem, plus loin |
| full mastery | +2,0 | Idem, plus loin |
Un palier solide avec une difficulté sous STRUGGLE_LOW ajoute +0,5 à λ (un cran d'étirement), jamais en examen.
Assemblage du prompt.
Deux couches. Le prompt système (SESSION_EXERCISES_SYSTEM_PROMPT, surchargeable en base via le registre de prompts) porte l'enveloppe JSON, les formes de data par type, les règles d'alignement, les règles strictes de correction des QCM et la règle de texte autosuffisant. Le prompt utilisateur est construit par appel par build_user_prompt() puis étendu par append_grid_plan().
| Bloc | Contenu | Notes |
|---|---|---|
| En-tête | Titre de la compétence · durée cible · cadence | |
| Objectif | competency_statement, à défaut le but du brief analysé | En-tête contraignant : le contenu de cours est déclaré matériau de référence, explicitement pas l'objet du test |
| Préférences | skill.learner_notes | Exclu en examen |
| Session | Type · nombre cible · ligne de style par type | |
| Notions | Libellé + balises [bloom: …] et [mastery: …] | Balises de maîtrise exclues en examen (mastery_aware = false) |
| Prose de calibration | Consignes par palier (nouveau/fragile, en cours, solide, dû mais compétent) | Par notion, pas par créneau |
| Contenu de cours | Versions de sections épinglées, tronquées à 6000 caractères chacune | Étiqueté « construire des tâches à partir, ne pas en tester le rappel » |
| Plan d'exercices | Une ligne numérotée par créneau : libellé · type imposé ou « choose ONE kind from … » · crochets facultatifs de vérification et de sonde | Déclare le plan FIXE en nombre et en ordre, et porte la consigne d'étayage des créneaux faded |
Paramètres de requête : json_mode = true, temperature = 0.7, max_tokens = 8192, plus un schéma JSON (exercises_response_schema()) sauf en mode libre.
Validation et persistance.
Jusqu'à MAX_ATTEMPTS = 3 cycles. Tous les problèmes d'une réponse sont collectés et renvoyés ensemble, pour qu'une seule régénération les corrige tous ; une erreur de fournisseur ou de réseau ne consomme pas d'essai.
- Nombre. La réponse doit contenir exactement un exercice par élément du plan.
- Type.
slot.allowed_kinds.contains(ex.kind), sinon l'élément est rejeté avec son index. C'est la frontière dure : l'IA ne peut pas échapper à la décision de palier de la grille. - Forme.
schema::validate_data(kind, data)rejette un contenu valide au schéma mais vide, par exemple unfill_blankdontexpectedest vide. Un élément fautif n'est jamais silencieusement écarté, car cela laisserait une session pédagogiquement incomplète. - Persistance.
write_exercises()supprime les exercices fraîchement rédigés de cette session puis réinsère, donc la régénération est idempotente. Les références réutilisées (session_exercise_refs.reused = 1) sont exclues de la cascade pour qu'un exercice partagé d'une session antérieure survive. - Tableaux parallèles.
scaffoldsettransfer_probessont alignés par index sur la liste fusionnée et écrits dansexercises.scaffold_leveletquestions.transfer_probe. Le drapeau de sonde est un fait de rédaction intrinsèque, lu identiquement par la soumission en direct et par le rejeu, donc une nouvelle correction ne peut pas diverger de la correction initiale. - Ce que cette boucle ne vérifie pas. Tout ce qui précède vérifie la structure : combien d'items sont revenus, de quel type est chacun, et si ses champs sont remplis et cohérents entre eux. Aucun de ces contrôles ne lit l'exercice. Rien ici ne décide si un énoncé est vrai, si une clé de correction est juste, si la question teste réellement la notion à laquelle elle est rattachée, ni même si la formulation permet de répondre. Ces règles existent — elles sont écrites dans le prompt de génération ci-dessus — mais un prompt énonce une règle, il ne l'applique pas, et c'est ici le seul endroit où l'application pourrait avoir lieu. C'est une vraie lacune, elle est nommée ici volontairement, et c'est pour cette raison qu'un exercice qui vous paraît faux ou absurde vaut la peine d'être signalé.
Composition de session (quelles notions).
En amont de la grille, compose.rs sélectionne quelles notions une session vise. Pur également. Les types de récupération recomposent à l'échelle de la compétence au lieu de faire confiance à la liste épinglée du plan.
- File des dues. Candidates dues triées par récupérabilité croissante, départagées par effet de levier sur le but plus élevé, puis stabilité plus faible, puis identifiant. Les plus en retard d'abord.
- Partage du budget.
budget_split(debt, k, mandate, nu). Un mandat Review est de la pure consolidation. Un mandat Course réserve un plancher de nouveau deround(k · 0,3 · (0,5 + nu)), borné à au moins 1 et au plus le plafond nominal, sauf si l'arriéré atteint2·k, auquel cas le contenu neuf est entièrement abandonné et la session devient du rattrapage. - Complément. Si la file des dues ne remplit pas le pool de révision, des éléments encodés mais non dus sont ajoutés, les plus proches de l'échéance d'abord. Les éléments jamais commencés sont exclus, car ce sont du contenu neuf et non de la révision.
- Entrelacement. Révision et nouveau sont alternés plutôt que regroupés, pour un espacement discriminatif.
- Ordre de présentation. L'étape ci-dessus ordonne les notions ; la grille émet ensuite tous les items d'une notion avant de passer à la suivante, si bien qu'une session composée en alternance pouvait malgré tout être servie par blocs. Une passe finale les espace, et elle dépend de ce que vous connaissez déjà : une notion que vous avez déjà travaillée voit ses items distribués en alternance avec les autres, tandis qu'une notion rencontrée pour la première fois garde ses items groupés. L'entrelacement sert à distinguer les méthodes entre elles, ce qui ne vaut son coût de commutation qu'une fois la méthode exécutable — la consolidation d'abord, la discrimination ensuite. Les deux groupes sont ensuite servis dans cet ordre : le contenu dû d'abord, le contenu neuf ensuite. Qui interrompt une session tôt perd ce qui venait en dernier, et une révision manquée coûte de la rétention sur du contenu déjà appris là où une première rencontre reportée ne coûte que de l'avancement.
Invariants imposés.
Ces propriétés tiennent par construction dans grid() et sont couvertes par des tests unitaires. Ce sont des propriétés de l'ensemble proposé : aucun choix de l'IA ne peut les violer.
| Invariant | Mécanisme |
|---|---|
| Aucun type sous le plancher Bloom | Filtre kind_violates_floor dans pick_kinds, revérifié à la validation |
| QCM ≤ 30 % | Borne dure sur l'ensemble proposé : mcq est retiré du choix une fois le plafond consommé |
| true_false uniquement en vérification | Absent de toutes les listes de membres ; seule l'insertion orthogonale l'émet |
| Écrit garanti pour evaluate / create | Le premier créneau Production renvoie ("writing", vec!["writing"]), imposé, sans choix |
| ≥ 3 types quand n ≥ 3 | Émergent de la décomposition en paliers ; vérifié par session |
| L'examen est aveugle à la maîtrise | Besoins uniformes, étayage solo, aucune insertion, mastery_aware = false |
| Chaque notion a ≥ 1 créneau | allocate_counts relève n au nombre de notions si nécessaire |
| Déterminisme | Même instantané + même salt → Vec<Slot> identique |
Manques connus.
Points ouverts sur cette surface, chacun avec sa localisation. Listés parce qu'un moteur documenté avec des trous non documentés est pire que pas de documentation du tout.
| Manque | Localisation | Effet |
|---|---|---|
| CLOS — Un budget, deux rôles | target_lp_count / LpBudget | Le même nombre fixait à la fois combien de notions une session touche et combien d'exercices elle rédige ; la répartition planchant à un item par notion, l'état stationnaire était exactement un exercice par notion, et quatre mécanismes adaptatifs (pondération par besoin, réduction de charge, le palier d'étayage faded → solo, le plafond de charge cognitive) devenaient inertes dès que le learner connaissait plus de notions que la durée de sa session n'avait de place. Les deux sont désormais des grandeurs distinctes : target_lp_count divise le budget d'exercices par TARGET_ITEMS_PER_LP (2), et le budget de notions est un newtype LpBudget — passer le compte d'exercices là où un compte de notions est attendu ne compile plus |
| CLOS — Un seul chemin de notation là où il en faut deux | apply_score_item / ItemContext::practice_repeat | Tout item noté était traité comme l'événement de planification de la notion, qu'il soit le premier de la session ou le septième deux minutes plus tard. Mesuré à l'époque : 8 items sur une notion dans une session achetaient +0,10 de stabilité contre un seul, et une récupération après un échec dans la même session verrouillait un intervalle de 2 jours là où celle du lendemain en gagnait 9. Une répétition à l'intérieur d'une session ne déplace désormais que le signal de compétence, et laisse l'horaire, les compteurs et les drapeaux de durabilité à l'événement de planification de la notion |
| CLOS — Insertions hors budget | grid() étape F | Ajoutées après la répartition, sans plafond ; mesuré, n0 = 10 avec 10 notions solides non vérifiées produisait 20 créneaux, dans le type le plus coûteux en temps. Les deux insertions additives sont désormais plafonnées à deux par session (MAX_INJECTS_PER_SESSION, MAX_PROBES_PER_SESSION) |
| L'explication reste facultative sur certains types | data_schema_for / les correcteurs déterministes | Sur 14 types d'exercices, 5 sont notés par un modèle de langage et portent un « pourquoi » exigé. Des 9 notés déterministiquement, 5 disposent désormais d'un champ pour une explication d'une phrase, et elle est exigée sur le contrôle d'idée fausse ; 4 n'ont toujours nulle part où la mettre, et là où le champ existe un modèle peut le laisser vide. Une erreur ne redit plus la réponse déjà marquée par la carte : sans explication, la carte le dit franchement et renvoie à la section qui enseigne le point |
| Tilt de profondeur inerte au sommet | apply_tilt / floor_clamp_and_normalize | Pour Evaluate et Create, tous les λ de −1 à +2 se normalisent en un mélange identique à 100 % Production, le plancher de ces niveaux étant déjà le palier supérieur. (La moitié Apply / Analyze de ce manque est corrigée : le tilt négatif vise désormais le plancher et non le palier en dessous) |
| Un type verrouillé à un domaine siège dans un palier général | Group::members | code_bug_spot n'est exclu qu'à Remember, et c'est le membre le plus chargé de Group::Analysis, qui porte 65 % du mix Analyze. Mesuré : sur une notion analyser, c'est le type préféré dans 4 à 5 créneaux sur 6, quel que soit le sujet. Jamais imposé (0 occurrence sur un balayage Bloom × palier × λ), donc l'IA peut s'en échapper, mais uniquement via la prose du prompt. Lié : le prompt système liste short_answer pour analyser, le palier Analyse de la grille ne le contient pas |
| Listes de membres aveugles au domaine | Group::members | code_trace / numeric_answer occupent les extrêmes de charge, donc deviennent le type préféré sur des notions non techniques ; seuls la prose du prompt et le choix de l'IA dans l'ensemble le rattrapent |
| Contradiction prompt / grille | prose de calibration | « favour recall and recognition » pour les notions nouvelles ou fragiles contredit le plancher Bloom, qui met ces paliers à zéro pour « appliquer » et au-dessus |
| La banque de termes contourne le plan | author_term_game | Les créneaux couverts par la banque sont retirés avant append_grid_plan : les consignes par créneau ne les atteignent pas |