diff --git a/docs/specs/decisions.md b/docs/specs/decisions.md index 5eb5887..35c76b4 100644 --- a/docs/specs/decisions.md +++ b/docs/specs/decisions.md @@ -136,3 +136,13 @@ Ajouts à l'arborescence proposée : - **Exclusion de `tests/` et `addons/gut/` de l'export** (`exclude_filter` des deux presets) : ce sont des dépendances de développement, elles n'ont rien à faire dans un exécutable livré à un enfant. Vérifié par la taille du `.pck` (124 Ko après exclusion contre 1,5 Mo avant). - **`export_presets.cfg` vérifié avant commit** : aucun champ de signature, de clé ou de chemin d'identifiants (ces champs n'existent que pour Android/iOS/macOS, hors périmètre). Rien à nettoyer. - **Non vérifié** : exécution réelle du binaire Windows (pas d'environnement Windows ni Wine disponible ici) ; comportement sur une distribution Linux différente de celle de cet environnement (cahier des charges §13 : « tester sur les distributions cibles et leurs dépendances graphiques ») ; macOS, Android, iOS restent hors périmètre de cette version. + +## 16. Bug signalé manuellement : correction de phrase trop stricte + +**Symptôme** : sur la mission « Le pont des phrases », toucher « petit » puis taper « petits » affichait « Pas tout à fait » au lieu d'être accepté. + +**Cause** : `AnswerValidator.validate_sentence_correction` comparait le texte saisi au texte attendu avec `!=`, une égalité de chaîne brute, sans aucune des tolérances déjà prévues pour les dictées (point 6) — pas de normalisation de casse, d'apostrophe typographique, d'espaces ou d'accents décomposés. Une différence incidente (ex. une majuscule ajoutée par une autocapitalisation du système) suffisait à rejeter une réponse pourtant correcte sur le fond (l'accord), ce qui contredit l'invariant pédagogique « une réponse ambiguë n'est pas déclarée fausse automatiquement ». + +**Correction** : la comparaison passe maintenant par `TextNormalizer.normalize(...).to_lower()` des deux côtés (même normalisation que pour les dictées), tout en gardant le texte brut saisi dans `found` pour l'affichage du retour. Un test automatisé avec la séquence exacte rapportée (toucher une fois, taper, valider) n'avait pas reproduit le bug — preuve que la logique fonctionnait pour une saisie identique au caractère près ; c'est la tolérance aux variations incidentes qui manquait, pas la logique d'association mot/correction elle-même. Trois tests couvrent maintenant la casse, les espaces superflus et les accents décomposés. + +**Leçon** : toute comparaison de texte saisi par l'enfant doit passer par `TextNormalizer`, pas par une égalité de chaîne brute — à vérifier si d'autres types d'exercices à saisie libre sont ajoutés plus tard. diff --git a/scripts/correction/answer_validator.gd b/scripts/correction/answer_validator.gd index 1fe591d..7e01860 100644 --- a/scripts/correction/answer_validator.gd +++ b/scripts/correction/answer_validator.gd @@ -48,7 +48,7 @@ static func validate_sentence_correction( errors.append(_missed_error(i, token, exercise)) elif is_faulty and was_selected: var submitted: String = corrections.get(i, "") - if submitted != token["expected"]: + if not _matches_expected(submitted, token["expected"]): errors.append(_wrong_correction_error(i, submitted, token, exercise)) elif not is_faulty and was_selected: errors.append(_false_positive_error(i, token, exercise)) @@ -65,6 +65,18 @@ static func validate_sentence_correction( ) +## Compare la correction saisie a celle attendue en ignorant les differences +## incidentes (majuscule/minuscule, apostrophe typographique, espaces, +## accents decomposes) : ce n'est pas la casse qui est evaluee ici, c'est +## l'accord. Une reponse ambigue n'est pas declaree fausse automatiquement +## (CLAUDE.md, invariants pedagogiques). +static func _matches_expected(submitted: String, expected: String) -> bool: + return ( + TextNormalizer.normalize(submitted).to_lower() + == TextNormalizer.normalize(expected).to_lower() + ) + + static func _missed_error(index: int, token: Dictionary, exercise: Dictionary) -> Dictionary: return { "position": index, diff --git a/tests/correction/test_answer_validator.gd b/tests/correction/test_answer_validator.gd index 39f5452..aa71ae5 100644 --- a/tests/correction/test_answer_validator.gd +++ b/tests/correction/test_answer_validator.gd @@ -75,3 +75,41 @@ func test_sentence_correction_false_positive_on_correct_token() -> void: assert_eq(result.errors.size(), 1) assert_eq(result.errors[0]["error_type"], "unclassified") assert_eq(result.errors[0]["position"], 2) + + +func test_sentence_correction_accepts_correction_with_different_case() -> void: + # Rapporte manuellement : "Petits" (majuscule incidente, ex. autocapitalisation) + # devait etre accepte au meme titre que "petits". + var result := AnswerValidator.validate_sentence_correction( + sentence_correction_exercise, [1], {1: "Petits"} + ) + assert_true(result.is_correct) + + +func test_sentence_correction_accepts_correction_with_surrounding_whitespace() -> void: + var result := AnswerValidator.validate_sentence_correction( + sentence_correction_exercise, [1], {1: " petits "} + ) + assert_true(result.is_correct) + + +func test_sentence_correction_accepts_decomposed_accented_correction() -> void: + var exercise_with_accent: Dictionary = { + "rule_id": "regle_test", + "hint": "indice", + "explanation": "explication", + "tokens": + [ + { + "text": "lecole", + "correct": false, + "expected": "école", + "rule_id": "regle_accord_adjectif", + }, + ], + } + # "e" + accent aigu combinant (U+0301), forme decomposee du meme mot. + var result := AnswerValidator.validate_sentence_correction( + exercise_with_accent, [0], {0: "école"} + ) + assert_true(result.is_correct)