Files
aventuriers-des-mots/scripts/correction/answer_validator.gd
T
Denis Defolie 91dcd7e07a 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>
2026-10-11 18:12:56 +02:00

112 lines
3.9 KiB
GDScript

class_name AnswerValidator
extends RefCounted
## Valide une reponse de choix multiple ou de correction de phrase et produit
## la structure de correction du §6 (voir CorrectionResult). Code pur : aucune
## dependance a l'arbre de scenes, aucune horloge, aucun hasard.
static func validate_multiple_choice(
exercise: Dictionary, selected_answer: int
) -> CorrectionResult:
var correct_answer: int = exercise["correct_answer"]
if selected_answer == correct_answer:
return CorrectionResult.correct(exercise["explanation"])
var answers: Array = exercise["answers"]
var found := ""
if selected_answer >= 0 and selected_answer < answers.size():
found = answers[selected_answer]
var expected: String = answers[correct_answer]
var error := {
"position": 0,
"error_type": "grammatical",
"rule_id": exercise["rule_id"],
"found": found,
"expected": expected,
}
return CorrectionResult.incorrect(
[error], exercise["explanation"], exercise["hint"], [expected]
)
## selected_indices : index des tokens que l'enfant a touches comme fautifs.
## corrections : correction saisie pour chaque token touche (cle = index du
## token, valeur = texte saisi).
static func validate_sentence_correction(
exercise: Dictionary, selected_indices: Array[int], corrections: Dictionary
) -> CorrectionResult:
var tokens: Array = exercise["tokens"]
var errors: Array[Dictionary] = []
for i in tokens.size():
var token: Dictionary = tokens[i]
var is_faulty: bool = token.get("correct", true) == false
var was_selected: bool = selected_indices.has(i)
if is_faulty and not was_selected:
errors.append(_missed_error(i, token, exercise))
elif is_faulty and was_selected:
var submitted: String = corrections.get(i, "")
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))
if errors.is_empty():
return CorrectionResult.correct(exercise["explanation"])
var remediation: Array[String] = []
for token: Dictionary in tokens:
if token.get("correct", true) == false:
remediation.append(token["expected"])
return CorrectionResult.incorrect(
errors, exercise["explanation"], exercise["hint"], remediation
)
## 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,
"error_type": "grammatical",
"rule_id": token.get("rule_id", exercise["rule_id"]),
"found": token["text"],
"expected": token["expected"],
}
static func _wrong_correction_error(
index: int, submitted: String, token: Dictionary, exercise: Dictionary
) -> Dictionary:
return {
"position": index,
"error_type": "grammatical",
"rule_id": token.get("rule_id", exercise["rule_id"]),
"found": submitted,
"expected": token["expected"],
}
static func _false_positive_error(
index: int, token: Dictionary, exercise: Dictionary
) -> Dictionary:
return {
"position": index,
"error_type": "unclassified",
"rule_id": exercise["rule_id"],
"found": token["text"],
"expected": token["text"],
}