mirror of
https://github.com/deunix-educ/aventuriers-des-mots.git
synced 2026-10-13 02:11:58 +02:00
Fix sentence correction rejecting valid answers over incidental text differences
AnswerValidator.validate_sentence_correction compared the typed correction to the expected word with raw string equality, with none of the tolerance already built for dictations: no case folding, no typographic-apostrophe or whitespace normalization, no decomposed- accent handling. Tapping "petit" and typing "petits" could fail for reasons having nothing to do with the grammar point being tested (e.g. a stray capital from autocapitalization), which violates the "an ambiguous answer is never auto-marked wrong" invariant. Found this while chasing a manual bug report: an automated test reproducing the user's exact click sequence (tap once, type, submit) did NOT reproduce the failure, which actually pointed at the real gap -- the logic worked for byte-identical input, but had zero tolerance for anything else. Now reuses TextNormalizer (built for dictation analysis) before comparing, case-insensitively, while still showing the child's raw typed text in the feedback. Three new tests cover case, incidental whitespace, and decomposed accents. Rebuilt both exports so the shipped binaries include the fix. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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).
|
- **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.
|
- **`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.
|
- **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.
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ static func validate_sentence_correction(
|
|||||||
errors.append(_missed_error(i, token, exercise))
|
errors.append(_missed_error(i, token, exercise))
|
||||||
elif is_faulty and was_selected:
|
elif is_faulty and was_selected:
|
||||||
var submitted: String = corrections.get(i, "")
|
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))
|
errors.append(_wrong_correction_error(i, submitted, token, exercise))
|
||||||
elif not is_faulty and was_selected:
|
elif not is_faulty and was_selected:
|
||||||
errors.append(_false_positive_error(i, token, exercise))
|
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:
|
static func _missed_error(index: int, token: Dictionary, exercise: Dictionary) -> Dictionary:
|
||||||
return {
|
return {
|
||||||
"position": index,
|
"position": index,
|
||||||
|
|||||||
@@ -75,3 +75,41 @@ func test_sentence_correction_false_positive_on_correct_token() -> void:
|
|||||||
assert_eq(result.errors.size(), 1)
|
assert_eq(result.errors.size(), 1)
|
||||||
assert_eq(result.errors[0]["error_type"], "unclassified")
|
assert_eq(result.errors[0]["error_type"], "unclassified")
|
||||||
assert_eq(result.errors[0]["position"], 2)
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user