Aller au contenu

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.

Déterministe Génération IA Prompt / validation

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.

build_lp_states() instantané par notion grid() étapes A–F → Slot[] author_term_game() banque de termes, sans IA append_grid_plan() plan → prompt l'IA rédige json_mode, T=0,7 valider + régénérer kind ∈ allowed_kinds · ×3 merge_local_and_llm ordre de la grille write_exercises() exercises + questions misconception : scan réel par notion créneaux additifs, plafonnés à 2 chacun scaffold_level atteint le rédacteur contourne le prompt de plan Les annotations sous chaque bloc disent ce que cette étape fait du plan. Les manques restants sont documentés en dernière section.

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.

A · budget n0 → n ×0,8 si gs ≥ 0,5 B · par notion poids de besoin plancher 1 chacune C · mélange % base_mix(bloom) tilt λ + plancher D · quantifier plus forts restes → 6 comptes entiers E · types plancher, plafond QCM → allowed_kinds F · extras échelle d'étayage + insertions L'étape F s'ajoute APRÈS la répartition de l'étape B. Un true_false de vérification par notion éligible, une sonde de transfert par notion solide non vérifiée de palier Bloom 3+. Ni l'un ni l'autre ne compte dans n ; chacun est plafonné à 2 par session. Mesuré : 10 notions solides non vérifiées avec n0 = 10 produisent 12 créneaux — 20 avant les plafonds.

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()

ChampTypeSource
lp_idStringidentifiant de la notion
bloomBloomBloom::from_opt(lp.bloom_level). Null retombe sur Understand
masteryf64lp.mastery
repsi64lp.reps
lapses_since_successi64lp.lapses_since_success
forecast_retentionf64mastery::forecast_retention(state, now)
is_dueboolmastery::is_due(state, now)
bandMasteryLevelmastery::level_of : New / Weak <0,5 / Developing <0,8 / Strong
misconceptionboolmastery::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_passedboollp.transfer_passed. Monotone, posé sur une sonde de transfert réussie

Slot, un exercice que l'IA doit rédiger

ChampSignificationAtteint le prompt ?
lp_idNotion visée ; résolue en son libellé pour le promptoui
kindType préféré de la grille ; toujours égal à allowed_kinds[0]. Aussi le type du chemin libre.oui
allowed_kindsTous 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_levelfaded 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_hinttrue_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 retouroui
transfer_probeSonde en contexte neuf imposée ; persistée dans questions.transfer_probeoui

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.

PalierGroupeTypes membres (charge croissante)
1Rappelflashcard
2Reconnaissancematching · mcq
3Rappel indicéfill_blank · short_answer · word_order
4Procéduralnumeric_answer · fill_blank · solve_equation · code_trace
5Analysecategorize · ordering · code_bug_spot
6Productionshort_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é.

BloomPlancherRappelRecon.IndicéProcéd.AnalyseProduction
Remember1402535000
Understand3520505200
Apply4001565155
Analyze5005106520
Evaluate600052570
Create600052075

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.

ConstanteValeurRôle
W_LAPSE / W_MAST / W_NEW0,45 / 0,35 / 0,20Poids de difficulté : s = W_LAPSE·min(lapses/3,1) + W_MAST·(1−mastery) + W_NEW·[reps==0]
LAPSE_NORM3,0Nombre 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_LOW0,6 / 0,3Levier de charge intra-palier : au-dessus → membre le moins chargé, en dessous → le plus chargé, entre → rotation
GS_HIGH / GS_LOAD0,5 / 0,8Difficulté 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_FADE2,0 / 1,0Poids de besoin par notion : 1 + 2·s + 1·is_due·(1−forecast_retention)
TILT_STEP8,0Points de pourcentage déplacés par unité de tilt de profondeur λ
MCQ_CAP0,30floor(0,30·n) créneaux peuvent se voir proposer mcq, à l'échelle de la session
MAX_INJECTS_PER_SESSION2Contrô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_SESSION2Sondes de transfert ajoutées par session, même règle de sélection
MAX_ATTEMPTS3Cycles générer → valider → régénérer avant échec explicite
DEBT_SUSPEND_FACTOR2,0Arriéré (en multiples du budget) à partir duquel une session de cours abandonne tout contenu neuf
NEW_FLOOR_SHARE0,3Fraction du budget réservée au contenu neuf avant mise à l'échelle par l'appétit de nouveauté
DEFAULT_NOVELTY0,5Appé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 sessionBase à 30 minComportement propre à la grille
Course / Rewrite7Poids de besoin 1 + 0,25·(palier−1) ; aucune insertion (le premier enseignement n'est pas la frontière de maîtrise)
Practice / Review10Review applique λ − 0,5, un biais de récupération plutôt qu'un étirement
Exam8Aveugle à la maîtrise : besoins uniformes, tout en solo, aucune insertion, pas de prose d'adaptation, préférences de l'apprenant retirées
Transfer4λ = 2,0 imposé : application au palier le plus haut
Recalibrate5Hors grille : chemin de prompt distinct
Project / Freeform3Hors grille
Placement6Hors 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,0Draine les paliers hauts vers le plancher relâché
(non défini)0,0Mélange de base inchangé
deep (practical)+1,0Draine les paliers bas vers le plus haut palier non vide
deep theoretical+1,5Idem, plus loin
full mastery+2,0Idem, 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().

BlocContenuNotes
En-têteTitre de la compétence · durée cible · cadence
Objectifcompetency_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érencesskill.learner_notesExclu en examen
SessionType · nombre cible · ligne de style par type
NotionsLibellé + balises [bloom: …] et [mastery: …]Balises de maîtrise exclues en examen (mastery_aware = false)
Prose de calibrationConsignes par palier (nouveau/fragile, en cours, solide, dû mais compétent)Par notion, pas par créneau
Contenu de coursVersions de sections épinglées, tronquées à 6000 caractères chacuneÉtiqueté « construire des tâches à partir, ne pas en tester le rappel »
Plan d'exercicesUne ligne numérotée par créneau : libellé · type imposé ou « choose ONE kind from … » · crochets facultatifs de vérification et de sondeDé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 un fill_blank dont expected est 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. scaffolds et transfer_probes sont alignés par index sur la liste fusionnée et écrits dans exercises.scaffold_level et questions.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.

  1. 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.
  2. 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 de round(k · 0,3 · (0,5 + nu)), borné à au moins 1 et au plus le plafond nominal, sauf si l'arriéré atteint 2·k, auquel cas le contenu neuf est entièrement abandonné et la session devient du rattrapage.
  3. 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.
  4. Entrelacement. Révision et nouveau sont alternés plutôt que regroupés, pour un espacement discriminatif.
  5. 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.

InvariantMécanisme
Aucun type sous le plancher BloomFiltre 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érificationAbsent de toutes les listes de membres ; seule l'insertion orthogonale l'émet
Écrit garanti pour evaluate / createLe 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îtriseBesoins uniformes, étayage solo, aucune insertion, mastery_aware = false
Chaque notion a ≥ 1 créneauallocate_counts relève n au nombre de notions si nécessaire
DéterminismeMême instantané + même saltVec<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.

ManqueLocalisationEffet
CLOS — Un budget, deux rôlestarget_lp_count / LpBudgetLe 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 deuxapply_score_item / ItemContext::practice_repeatTout 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 budgetgrid() étape FAjouté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 typesdata_schema_for / les correcteurs déterministesSur 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 sommetapply_tilt / floor_clamp_and_normalizePour 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éralGroup::memberscode_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 domaineGroup::memberscode_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 / grilleprose 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 planauthor_term_gameLes créneaux couverts par la banque sont retirés avant append_grid_plan : les consignes par créneau ne les atteignent pas