Chapitre 4.4 — Statut, score & suspend_data
⏱️ TL;DR — Trois champs décident du sort de ton module. Le statut dit si l’apprenant a terminé (et réussi) : c’est le piège n°1 du e-learning — un SCO qui reste bloqué sur
incompleteparce que le code ne pose jamaiscompleted/passed. Le score doit être posé dans le bon champ et la bonne échelle :cmi.core.score.raw(0–100) en 1.2,cmi.score.scaled(-1à1) en 2004. Etsuspend_dataest la mémoire libre du SCO : tu y sérialises l’état pour permettre la reprise — mais avec une limite de taille (4096 caractères en 1.2, 64000 en 2004) qui impose un format compact. Complété parlesson_location/location(le signet). Rate l’un de ces trois, et c’est « ça marche à l’écran mais rien ne compte ».
🎯 Objectifs
- Poser correctement le statut :
lesson_statusen 1.2, le couplecompletion_status+success_statusen 2004. - Éviter le piège n°1 : le module qui reste
incompletefaute de passercompleted. - Choisir le bon champ et la bonne échelle de score (raw vs scaled), et gérer un
masteryScore. - Utiliser
suspend_dataetlesson_location/locationpour une reprise fiable, en restant sous la limite de taille.
Le piège n°1 : rester incomplete
Le scénario, on l’a vu sur FormaCampus : le module se déroule parfaitement, l’apprenant arrive au bout, ferme… et le LMS affiche toujours « en cours ». Neuf fois sur dix, la cause est le statut jamais mis à completed.
Le statut ne devient pas completed tout seul. Aucun LMS ne « devine » qu’un apprenant a fini parce qu’il a vu le dernier écran. C’est au SCO de le déclarer, explicitement, par un SetValue sur le bon champ, puis de committer et clore (chapitre 4.2).
En SCORM 1.2 : un seul champ, lesson_status
cmi.core.lesson_status porte tout. Son vocabulaire fermé :
| Valeur | Sens |
|---|---|
not attempted | Jamais commencé. |
incomplete | Commencé, pas fini. État de départ typique. |
browsed | Parcouru en mode consultation. |
completed | Terminé (sans notion de réussite). |
passed | Terminé et réussi (score au-dessus du seuil). |
failed | Terminé et échoué. |
La bonne séquence : poser incomplete à l’ouverture, puis completed (ou passed/failed s’il y a un seuil) à la fin.
// SCORM 1.2 — le statut au bon moment
api.LMSInitialize("");
api.LMSSetValue("cmi.core.lesson_status", "incomplete"); // départ
api.LMSCommit("");
// ... plus tard, l'apprenant a fini le module (contenu sans note) ...
api.LMSSetValue("cmi.core.lesson_status", "completed"); // LE geste qui manquait
api.LMSCommit("");
api.LMSFinish("");S’il y a un score seuil (masteryScore), on tranche passed/failed :
// Avec un seuil : le SCO calcule passed/failed
const seuil = Number(api.LMSGetValue("cmi.core.student_data.mastery_score")) || 80;
const note = 74;
api.LMSSetValue("cmi.core.score.raw", String(note));
api.LMSSetValue(
"cmi.core.lesson_status",
note >= seuil ? "passed" : "failed"
);
api.LMSCommit("");⚠️ Piège — Laisser le statut à
incomplete(ou pire, ne jamais l’écrire, doncnot attempted). C’est la cause n°1 des tickets « le module ne valide pas ». Le contenu peut être irréprochable : sansSetValue("...completed"/"passed"), le LMS n’a aucune raison de valider. Le statut est un acte explicite, jamais un effet de bord.
En SCORM 2004 : deux champs séparés
2004 découple la complétion (a-t-on parcouru le contenu ?) de la réussite (a-t-on atteint le seuil ?). Il faut poser les deux — sinon l’un des deux reste unknown.
// SCORM 2004 — complétion ET réussite, séparément
api.Initialize("");
api.SetValue("cmi.completion_status", "incomplete"); // départ
api.SetValue("cmi.success_status", "unknown");
api.Commit("");
// ... à la fin, avec un score et un seuil de 0,8 (échelle scaled -1..1) ...
const scaled = 0.74;
api.SetValue("cmi.score.scaled", String(scaled));
api.SetValue("cmi.completion_status", "completed"); // parcouru
api.SetValue("cmi.success_status", scaled >= 0.8 ? "passed" : "failed"); // réussi ?
api.Commit("");
api.Terminate("");Cette séparation permet des états impossibles en 1.2 : « terminé mais échoué » (completed + failed), ou « réussi sans être marqué terminé » — utile quand la complétion et la note obéissent à des règles distinctes.
💡 Réflexe — Décide à l’avance ce que « terminé » veut dire pour ton module (vu tous les écrans ? répondu au quiz ? atteint le seuil ?) et écris-le noir sur blanc dans le code, au bon moment. « Terminé » est une règle métier, pas une intuition. En 2004, n’oublie jamais de poser les deux statuts.
Le score : raw, scaled, et le seuil
| Aspect | SCORM 1.2 | SCORM 2004 |
|---|---|---|
| Champ principal | cmi.core.score.raw (souvent 0–100) | cmi.score.scaled (-1 à 1) |
| Bornes | cmi.core.score.min / .max | cmi.score.min / .max (+ raw) |
| Seuil de réussite | cmi.core.student_data.mastery_score | masteryScore (manifeste / séquencement) |
Deux erreurs fréquentes :
- Envoyer un
rawsansmin/max: un score « 42 » n’a de sens que si on sait qu’il est sur 50 ou sur 100. Poseminetmaxpour lever l’ambiguïté. - Confondre les échelles : en 2004,
cmi.score.scaledest un ratio, pas un pourcentage entier. 85 % s’écrit"0.85", pas"85". Un"85"dansscaledest hors plage et sera rejeté.
// Bien poser un score en SCORM 2004
const note = 85, total = 100;
api.SetValue("cmi.score.raw", String(note)); // "85"
api.SetValue("cmi.score.min", "0");
api.SetValue("cmi.score.max", String(total)); // "100"
api.SetValue("cmi.score.scaled", (note / total).toFixed(2)); // "0.85"suspend_data : la mémoire libre du SCO
cmi.suspend_data (1.2 et 2004) est un champ libre : le LMS le stocke tel quel et te le rend à la reprise, sans jamais l’interpréter. C’est ta mémoire pour reconstruire l’état exact du module : réponses déjà données, écrans visités, progression fine, tirage aléatoire des questions… Tout ce dont tu as besoin pour que l’apprenant reprenne où il s’était arrêté.
Mais il y a une limite de taille stricte :
| Version | Taille max de suspend_data |
|---|---|
| SCORM 1.2 | 4096 caractères |
| SCORM 2004 | 64000 caractères |
4096 caractères, c’est très peu. Un JSON verbeux les explose en quelques dizaines de questions. La discipline : sérialiser compact.
// À NE PAS FAIRE en 1.2 : JSON verbeux, explose vite les 4096 caractères
const gros = JSON.stringify({
pageCourante: 7,
reponses: [
{ question: "q1", choix: "b", correct: true },
{ question: "q2", choix: "a", correct: false }
// ... 40 questions et c'est mort
]
});// MIEUX : format positionnel ultra-compact (à toi de le documenter)
// "7|10110..." = page 7, puis 1 bit par question (1 = correct)
const page = 7;
const bits = [1, 0, 1, 1, 0]; // résultats des questions
const compact = page + "|" + bits.join("");
api.LMSSetValue("cmi.suspend_data", compact);
api.LMSCommit("");À la reprise, on relit et on reconstruit :
// Reprise : lire le contexte d'entrée, puis restaurer depuis suspend_data
const entry = api.LMSGetValue("cmi.core.entry"); // "resume" si reprise
if (entry === "resume") {
const brut = api.LMSGetValue("cmi.suspend_data");
const [page, bits] = brut.split("|");
restaurerEtat(Number(page), bits.split("").map(Number));
}⚠️ Piège — Dépasser les 4096 caractères en SCORM 1.2. Selon le LMS, la valeur est tronquée (état corrompu à la reprise) ou rejetée (rien n’est sauvé). Symptôme : « la reprise remet l’apprenant au début » ou « plante après X questions ». Toujours mesurer
suspend_data.lengthavant d’écrire, et prévoir un format qui tient dans le budget.
lesson_location : le signet
cmi.core.lesson_location (1.2) / cmi.location (2004) est le signet court : un repère de où reprendre (numéro d’écran, identifiant de section). À distinguer de suspend_data :
lesson_location= « où j’en étais » (court :"ecran-7").suspend_data= « dans quel état j’étais » (riche : réponses, progression…).
// À chaque changement d'écran : mettre à jour le signet
api.LMSSetValue("cmi.core.lesson_location", "ecran-7");
api.LMSSetValue("cmi.core.exit", "suspend"); // "je pars mais je reprendrai"
api.LMSCommit("");Le cmi.core.exit à suspend (1.2) / cmi.exit à suspend (2004) signale au LMS que la sortie est une mise en pause : c’est ce qui fait que cmi.core.entry vaudra resume à la prochaine ouverture, déclenchant ta logique de reprise.
🧭 Sur FormaCampus — Diagnostic bouclé. Le module fautif avait deux bugs : (1)
LMSFinishjamais appelé en fermeture d’onglet (chapitre 4.2), et (2)cmi.core.lesson_statusjamais posé àcompleted— le code se contentait deincompleteà l’ouverture et n’écrivait rien à la fin, parce que la « fin » était un écran de félicitations sans appel d’API. Correction en deux gestes : posercmi.core.lesson_statusàcompleted(oupassedsi la note atteint lemasteryScore) au moment où l’apprenant valide le dernier écran, puisLMSCommit+LMSFinishgarantis viapagehide. Résultat côté Moodle de l’école : la complétion remonte enfin, les stagiaires passent en « terminé », l’enseignant voit les scores. Un ticket client fermé grâce à troisSetValuebien placés.
📚 La spec — Les vocabulaires fermés (
lesson_status,completion_status,success_status), les plages de score (scaledde-1à1) et les tailles maximales desuspend_data(4096 en 1.2, 64000 en 2004) sont fixés dans le livre RTE de chaque version SCORM (ADL,adlnet.gov). LemasteryScoreet la logique de « satisfaction » relèvent aussi du CAM (manifeste) et, en 2004, du séquencement — voir Partie 5.
✏️ Exercices
Exercice 1 — Pourquoi ça ne valide pas ? Un SCO 1.2 fait exactement ceci et l’école se plaint que « rien ne passe en terminé » :
api.LMSInitialize("");
api.LMSSetValue("cmi.core.lesson_status", "incomplete");
// ... l'apprenant parcourt tout et arrive à l'écran final ...
api.LMSCommit("");
api.LMSFinish("");Diagnostique et corrige.
✅ Solution
Le statut est posé à incomplete à l’ouverture et jamais remis à completed/passed à la fin. Le Commit/Finish sont bien là, mais ils persistent un statut… incomplete. Le LMS fait exactement ce qu’on lui dit : « en cours ». C’est le piège n°1.
Correction : écrire le statut final avant de clore.
api.LMSInitialize("");
api.LMSSetValue("cmi.core.lesson_status", "incomplete");
// ... à l'écran final, l'apprenant valide ...
api.LMSSetValue("cmi.core.lesson_status", "completed"); // le geste manquant
api.LMSSetValue("cmi.core.exit", ""); // sortie normale
api.LMSCommit("");
api.LMSFinish("");Exercice 2 — Budget suspend_data. En SCORM 1.2, tu dois mémoriser, pour reprise, la page courante et le résultat (correct/incorrect) de 300 questions. Un JSON.stringify d’objets {question, correct} fait ~9000 caractères. Propose un format qui tient dans 4096.
✅ Solution
Un objet par question est ~30× trop gros. Un format positionnel à 1 caractère par question suffit : l’indice encode la question, le caractère encode le résultat.
// "42#1011001..." : page 42, puis 300 caractères (1 = correct, 0 = incorrect)
const page = 42;
const bits = resultats.map((r) => (r ? "1" : "0")).join(""); // 300 caractères
const data = page + "#" + bits; // ~304 caractères, très loin des 4096
api.LMSSetValue("cmi.suspend_data", data);300 caractères pour 300 questions, contre 9000 en JSON. On peut compresser encore (bitmask base64) si besoin, mais l’idée clé est : 1 question = 1 caractère, et on documente le format. Toujours vérifier data.length < 4096 avant d’écrire.
Exercice 3 — 1.2 vs 2004 : le statut. Un apprenant a tout parcouru mais a échoué au test final (score sous le seuil). Écris le statut correct en 1.2 et en 2004.
✅ Solution
SCORM 1.2 — un seul champ ; « terminé mais échoué » se dit failed (qui implique la complétion) :
api.LMSSetValue("cmi.core.lesson_status", "failed");SCORM 2004 — on sépare : parcouru et échoué :
api.SetValue("cmi.completion_status", "completed"); // il a tout parcouru
api.SetValue("cmi.success_status", "failed"); // mais raté le seuilC’est précisément le cas que 2004 exprime mieux que 1.2 : en 1.2, failed mélange les deux idées ; en 2004, on distingue « a fini » de « a réussi ».
🧠 Quiz de révision
1. Quel est le piège n°1 du runtime SCORM ?
Le statut qui reste incomplete (ou not attempted) parce que le code ne pose jamais completed/passed à la fin. Le LMS ne devine pas la complétion : c’est un SetValue explicite, suivi de Commit/Terminate.
2. En SCORM 2004, combien de champs de statut faut-il poser, et lesquels ?
Deux : cmi.completion_status (completed/incomplete/…) pour la complétion, et cmi.success_status (passed/failed/unknown) pour la réussite. Oublier l’un laisse l’autre à unknown.
3. Comment s’écrit 85 % dans cmi.score.scaled ?
cmi.score.scaled ?"0.85". cmi.score.scaled est un ratio de -1 à 1, pas un pourcentage entier. "85" serait hors plage et rejeté. (En 1.2, cmi.core.score.raw vaut bien "85" sur 100.)
4. Quelle est la taille maximale de suspend_data ?
suspend_data ?4096 caractères en SCORM 1.2, 64000 en SCORM 2004. En 1.2 surtout, il faut sérialiser compact (format positionnel, 1 caractère par item) et vérifier la longueur avant d’écrire.
5. Quelle différence entre lesson_location et suspend_data ?
lesson_location et suspend_data ?lesson_location/location = où reprendre (signet court, ex. "ecran-7"). suspend_data = dans quel état reprendre (données riches : réponses, progression). On combine les deux, avec exit=suspend pour que la prochaine entry vaille resume.
Chapitre suivant : 4.5 — Débuguer le runtime — quand malgré tout « rien ne remonte » : lire les erreurs, dérouler la checklist, isoler le SCO muet.