diff --git a/data/localization/fr.json b/data/localization/fr.json index 6efda91..fd7baf0 100644 --- a/data/localization/fr.json +++ b/data/localization/fr.json @@ -38,6 +38,7 @@ "feedback_panel.correction_label": "La bonne réponse :", "sentence_correction.correction_placeholder": "Corrige ce mot", "dictation.instruction_label": "Écris la phrase que tu entends :", + "dictation.convention_hint": "N'oublie pas : une majuscule au début de la phrase et un point à la fin !", "dictation.input_placeholder": "Écris ici...", "dictation.replay_button": "Réécouter", "dictation.provisional_audio_badge": "Voix provisoire", diff --git a/docs/specs/decisions.md b/docs/specs/decisions.md index 99ed5eb..e4dff0d 100644 --- a/docs/specs/decisions.md +++ b/docs/specs/decisions.md @@ -196,3 +196,15 @@ Ajouts à l'arborescence proposée : **Correction** : `DictationAnalyzer` ignore désormais la ponctuation terminale (`.`, `!`, `?`, `…`) à la comparaison — aussi bien dans le test d'égalité exacte (et celui des variantes acceptées) que dans l'alignement mot à mot, où elle est retirée avant la découpe en mots plutôt qu'après coup. La ponctuation interne à la phrase n'est pas concernée. Quatre tests couvrent : omission du point, point remplacé par un autre signe, et absence de faux négatif (une vraie faute sur le dernier mot reste détectée malgré la tolérance de ponctuation). **Leçon** : une tolérance de comparaison doit correspondre à ce que le canal d'entrée (ici l'audio) peut réellement transmettre à l'enfant — exiger une information qu'il n'a pas reçue revient à le punir d'une faute qu'il n'a pas commise (CLAUDE.md, invariants pédagogiques). À revoir si l'audio final fait un jour entendre la ponctuation (pause marquée, voix qui annonce « point »). + +**Revu au point 22** : cette tolérance a été annulée après discussion — majuscule et point final sont une convention générale, pas une information dictée. + +## 22. Majuscule et point final : exigés comme convention générale, pas tolérés + +**Contexte** : en poursuivant le test manuel du point 21, une réponse tapée sans majuscule initiale (« les petits chats dorment ») a aussi échoué, sur une erreur de catégorie « capitalization ». Question posée (CLAUDE.md : « en cas d'ambiguïté, poser la question plutôt que trancher seul ») plutôt que d'étendre seul la tolérance du point 21 à la majuscule : la majuscule en début de phrase et le point final sont-ils une information que l'audio devrait transmettre (comme supposé au point 21), ou une convention générale que l'enfant applique de lui-même, indépendamment de ce qu'il entend — comme en classe, où l'enseignant ne dit pas toujours « majuscule » ou « point » avant chaque dictée ? + +**Décision** : convention générale. Les deux sont exigés, comme n'importe quel autre mot de la phrase. La tolérance du point 21 est annulée : `DictationAnalyzer` retrouve sa comparaison d'origine (plus de `_strip_terminal_punctuation`), majuscule et point final doivent à nouveau être écrits exactement. + +**Contrepartie** : exiger une convention que l'audio ne signale pas suppose que l'enfant la connaît déjà par ailleurs, pas qu'il la devine dictée par dictée. Ajout d'un rappel permanent et visible dans la scène de dictée (`dictation.tscn`, label `ConventionHintLabel`, clé `dictation.convention_hint` dans `data/localization/fr.json` : « N'oublie pas : une majuscule au début de la phrase et un point à la fin ! »). C'est la réponse concrète à « il faudra indiquer les règles quelque part » : la règle est communiquée par l'interface, pas supposée acquise silencieusement. + +**Tests** : `tests/correction/test_dictation_analyzer.gd` vérifie que l'omission du point est à nouveau une erreur (la majuscule l'était déjà, test existant) ; `tests/exercises/test_exercise_host_dictation.gd` vérifie la même chose au niveau de la scène complète. diff --git a/docs/testing/manual-checklist.md b/docs/testing/manual-checklist.md index e721299..7ada652 100644 --- a/docs/testing/manual-checklist.md +++ b/docs/testing/manual-checklist.md @@ -14,7 +14,7 @@ Tout ce qui suit ne peut pas être vérifié sans écran (rendu, mise en page, a 8. Mission « Le pré des petits mots » (choix multiple) : les réponses sont cliquables, une réponse juste affiche « Bravo ! », une réponse fausse affiche « Pas tout à fait » avec un indice. 9. Refaire une erreur sur le même exercice : after la 2e erreur, l'explication remplace l'indice ; après la 3e, la bonne réponse est montrée et l'exercice passe au suivant sans blocage. 10. Mission « Le pont des phrases » (correction de phrase) : toucher un mot le fait apparaître comme sélectionné (état visuel distinct, pas seulement une couleur) ; un champ de saisie apparaît sous le mot sélectionné ; toucher à nouveau le mot fait disparaître le champ. -11. Mission « La clairière de l'écho » (dictée) : à l'arrivée sur l'exercice, la voix de synthèse doit se faire entendre automatiquement ; le bouton « Réécouter » la rejoue sans planter ; le badge « Voix provisoire » est visible ; la zone de saisie accepte le texte et les accents. Taper le texte sans point final (rien dans l'audio ne l'indique) doit être accepté comme correct (point 21 de `decisions.md`). +11. Mission « La clairière de l'écho » (dictée) : à l'arrivée sur l'exercice, la voix de synthèse doit se faire entendre automatiquement ; le bouton « Réécouter » la rejoue sans planter ; le badge « Voix provisoire » est visible ; la zone de saisie accepte le texte et les accents. Le rappel « N'oublie pas : une majuscule au début de la phrase et un point à la fin ! » doit être visible (point 22 de `decisions.md`) ; taper le texte sans majuscule ou sans point final doit être refusé, avec l'erreur correspondante. 12. Après 3 exercices, écran de récompense avec le nombre d'étoiles, puis retour à la carte. 13. Quitter le jeu puis relancer `make run` : le profil et sa progression doivent être conservés (sélectionner le même profil, vérifier que l'état affiché sur la carte a changé par rapport à l'état initial). 14. Depuis la sélection de profil, supprimer un profil : une confirmation explicite doit apparaître avant la suppression effective ; annuler doit ne rien supprimer. diff --git a/scenes/exercises/dictation.gd b/scenes/exercises/dictation.gd index 169f810..269dae4 100644 --- a/scenes/exercises/dictation.gd +++ b/scenes/exercises/dictation.gd @@ -3,6 +3,10 @@ extends Control ## par signal, ne touche ni a la sauvegarde ni a la progression. L'audio est ## provisoire (voir docs/specs/decisions.md, point 8) ; le badge l'indique a ## l'enfant tant qu'aucune voix definitive n'a remplace le fichier. +## +## Majuscule initiale et point final sont exiges a la correction (point 22) +## sans etre entendus dans l'audio : le rappel ci-dessous est la facon dont +## l'enfant en est informe, pas une tolerance ajoutee au validateur. signal answer_submitted(submitted_text: String) @@ -11,6 +15,7 @@ var _dictation: Dictionary = {} @onready var _instruction_label: Label = %InstructionLabel @onready var _replay_button: Button = %ReplayButton @onready var _provisional_badge: Label = %ProvisionalBadge +@onready var _convention_hint_label: Label = %ConventionHintLabel @onready var _text_edit: LineEdit = %TextEdit @onready var _validate_button: Button = %ValidateButton @@ -20,6 +25,7 @@ func _ready() -> void: _replay_button.text = UIText.get_text("dictation.replay_button") _text_edit.placeholder_text = UIText.get_text("dictation.input_placeholder") _provisional_badge.text = UIText.get_text("dictation.provisional_audio_badge") + _convention_hint_label.text = UIText.get_text("dictation.convention_hint") _validate_button.text = UIText.get_text("exercise_host.validate_button") _replay_button.pressed.connect(_on_replay_pressed) diff --git a/scenes/exercises/dictation.tscn b/scenes/exercises/dictation.tscn index 7f5f106..fc218e5 100644 --- a/scenes/exercises/dictation.tscn +++ b/scenes/exercises/dictation.tscn @@ -37,6 +37,11 @@ unique_name_in_owner = true layout_mode = 2 theme_override_colors/font_color = Color(0.6, 0.45, 0.1, 1) +[node name="ConventionHintLabel" type="Label" parent="Margin/VBox"] +unique_name_in_owner = true +layout_mode = 2 +autowrap_mode = 2 + [node name="ReplayButton" type="Button" parent="Margin/VBox"] unique_name_in_owner = true layout_mode = 2 diff --git a/scripts/correction/dictation_analyzer.gd b/scripts/correction/dictation_analyzer.gd index 5d45131..e9039d4 100644 --- a/scripts/correction/dictation_analyzer.gd +++ b/scripts/correction/dictation_analyzer.gd @@ -7,13 +7,13 @@ extends RefCounted ## classee (voir "Points durs" du prompt de lancement). Code pur, sans horloge ## ni hasard. L'explication complete (texte de la regle) est composee par la ## couche d'affichage a partir des rule_id retournes, pas ici. - -## Ponctuation de fin de phrase ignoree a la comparaison : l'audio provisoire -## (point 19/20 de docs/specs/decisions.md) ne la fait pas entendre, donc rien -## dans ce que l'enfant ecoute ne lui indique qu'il faut l'ecrire. L'exiger -## serait sanctionner une information qu'il n'a pas recue (CLAUDE.md : une -## erreur declenche une explication, jamais une punition). -const TERMINAL_PUNCTUATION := ".!?…" +## +## Majuscule initiale et point final sont exiges comme n'importe quel autre +## mot : ce sont des conventions generales d'ecriture (toute phrase commence +## par une majuscule et finit par un point), pas une information que l'audio +## devrait faire entendre. L'enfant en est informe par le rappel affiche dans +## la scene de dictee (voir docs/specs/decisions.md, point 22), pas par +## tolerance a la correction. static func analyze(dictation: Dictionary, submitted_text: String) -> CorrectionResult: @@ -23,10 +23,8 @@ static func analyze(dictation: Dictionary, submitted_text: String) -> Correction if _matches_target_or_variant(dictation, normalized_submitted, normalized_target): return CorrectionResult.correct(_build_explanation(0)) - var target_words := TextNormalizer.split_words(_strip_terminal_punctuation(normalized_target)) - var submitted_words := TextNormalizer.split_words( - _strip_terminal_punctuation(normalized_submitted) - ) + var target_words := TextNormalizer.split_words(normalized_target) + var submitted_words := TextNormalizer.split_words(normalized_submitted) var words_meta: Array = dictation["words"] # L'alignement se fait insensible a la casse : une majuscule manquante est @@ -83,27 +81,14 @@ static func analyze(dictation: Dictionary, submitted_text: String) -> Correction static func _matches_target_or_variant( dictation: Dictionary, normalized_submitted: String, normalized_target: String ) -> bool: - var stripped_submitted := _strip_terminal_punctuation(normalized_submitted) - if stripped_submitted == _strip_terminal_punctuation(normalized_target): + if normalized_submitted == normalized_target: return true for variant: String in dictation.get("accepted_variants", []): - if stripped_submitted == _strip_terminal_punctuation(TextNormalizer.normalize(variant)): + if normalized_submitted == TextNormalizer.normalize(variant): return true return false -## Retire les signes de ponctuation terminale (un ou plusieurs, ex. "?!") en -## fin de texte. N'affecte pas la ponctuation interne a la phrase. -static func _strip_terminal_punctuation(text: String) -> String: - var result := text - while ( - not result.is_empty() - and TERMINAL_PUNCTUATION.find(result.substr(result.length() - 1)) != -1 - ): - result = result.substr(0, result.length() - 1) - return result.strip_edges() - - static func _classify_error(words_meta: Array, target_index: int, found: String) -> Dictionary: var meta: Dictionary = words_meta[target_index] if target_index < words_meta.size() else {} var expected: String = meta.get("text", "") diff --git a/tests/correction/test_dictation_analyzer.gd b/tests/correction/test_dictation_analyzer.gd index 238a720..cd0a6b7 100644 --- a/tests/correction/test_dictation_analyzer.gd +++ b/tests/correction/test_dictation_analyzer.gd @@ -67,21 +67,13 @@ func test_normalizes_before_comparing() -> void: assert_true(result.is_correct) -## L'audio provisoire ne fait entendre aucune ponctuation (voir -## docs/specs/decisions.md, point 19/20) : l'omettre n'est pas une erreur. -func test_missing_terminal_punctuation_is_still_correct() -> void: +## Majuscule initiale et point final sont des conventions generales d'ecriture +## (voir docs/specs/decisions.md, point 22), exigees comme n'importe quel +## autre mot : l'enfant en est informe par un rappel affiche, pas par une +## tolerance a la correction. +func test_missing_terminal_punctuation_is_an_error() -> void: var result := DictationAnalyzer.analyze(dictation, "Les petits chats dorment") - assert_true(result.is_correct) - - -func test_different_terminal_punctuation_is_still_correct() -> void: - var result := DictationAnalyzer.analyze(dictation, "Les petits chats dorment!") - assert_true(result.is_correct) - - -func test_terminal_punctuation_tolerance_does_not_hide_a_real_error_on_the_last_word() -> void: - var result := DictationAnalyzer.analyze(dictation, "Les petits chats dorme") assert_false(result.is_correct) assert_eq(result.errors.size(), 1) - assert_eq(result.errors[0]["found"], "dorme") + assert_eq(result.errors[0]["found"], "dorment") assert_eq(result.errors[0]["expected"], "dorment.") diff --git a/tests/exercises/test_exercise_host_dictation.gd b/tests/exercises/test_exercise_host_dictation.gd index 7006765..6f1d8c1 100644 --- a/tests/exercises/test_exercise_host_dictation.gd +++ b/tests/exercises/test_exercise_host_dictation.gd @@ -1,7 +1,8 @@ extends GutTest -## Reproduction d'un bug signale manuellement via exercise_host : la dictee -## "Les petits chats dorment." tapee exactement comme entendue n'etait pas -## validee comme correcte. +## Reproduction de deux retours de test manuel via exercise_host, sur la +## dictee "Les petits chats dorment." (voir docs/specs/decisions.md, points +## 21 et 22) : la reponse exacte doit valider, et la majuscule/le point final +## restent exiges (conventions generales d'ecriture, pas une tolerance). var _profile_id: String = "" @@ -30,10 +31,7 @@ func test_exact_target_text_is_validated_as_correct() -> void: assert_eq(_feedback_title(host), UIText.get_text("feedback_panel.correct_title")) -## "J'entends parfaitement mais ne valide pas" : l'audio ne fait entendre -## aucune ponctuation, donc l'enfant qui ecrit exactement ce qu'il entend -## (sans le point final) doit tout de meme etre valide. -func test_missing_final_period_is_still_validated_as_correct() -> void: +func test_missing_final_period_is_not_validated_as_correct() -> void: var host: Control = await _ready_host() var exercise_scene: Control = host.get_node("%ExerciseContainer").get_child(0) @@ -41,7 +39,7 @@ func test_missing_final_period_is_still_validated_as_correct() -> void: exercise_scene.get_node("%ValidateButton").pressed.emit() await wait_process_frames(1) - assert_eq(_feedback_title(host), UIText.get_text("feedback_panel.correct_title")) + assert_eq(_feedback_title(host), UIText.get_text("feedback_panel.incorrect_title")) func _ready_host() -> Control: