Add profile and save persistence, wired into the three autoloads

scripts/profiles/: SaveIO handles real file I/O in user://saves (one
file per profile_id, atomic write via temp-file-then-rename, backup
copy before each overwrite, corrupted-main falls back to backup,
schema-invalid content is rejected like corruption). SaveMigration
and SaveFactory are pure; SaveMigration refuses a schema_version it
doesn't recognize (older or newer) rather than guessing. ProgressUpdates
is pure and wires ReviewScheduler/DifficultyManager onto a save's
skills dict, including per-session overdue-review marking.

The three autoloads (SaveManager, ProfileManager, ProgressManager)
stay thin: in-memory current-profile state and orchestration only,
no logic of their own, so the actual behavior lives in the tested
pure/file-IO modules above rather than in untestable glue.

26 new headless tests, including the failure modes called out in the
launch prompt: a truncated .tmp file never corrupts the readable
save, a corrupted main file falls back to its backup, two profiles
never mix (down through the real autoload layer, not just SaveIO),
and an unknown future schema_version is rejected outright.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Denis Defolie
2026-10-11 17:12:50 +02:00
parent 2fe455f5d1
commit ebabe91821
22 changed files with 696 additions and 6 deletions
+9
View File
@@ -105,3 +105,12 @@ Ajouts à l'arborescence proposée :
- `DictationAnalyzer` aligne les mots en comparant une version en minuscules (alignement insensible à la casse) : sinon une majuscule manquante en début de mot est vue comme une substitution par `WordAligner` et jamais testée comme erreur de capitalisation à part (point 6 des décisions). Les mots originaux (avec leur casse) restent utilisés pour construire les erreurs et pour la vérification de majuscule.
- `ReviewScheduler.record_attempt` est une fonction pure qui duplique l'état (`Dictionary.duplicate(true)`) plutôt que de le modifier en place ; `ProgressManager` (étape « Profils et sauvegardes ») sera responsable de stocker l'état retourné.
- Le planning de révision est entièrement piloté par le numéro de séance (`current_session`), jamais par une horloge : paliers `[1, 3, 6, 12]` séances après la dernière réussite à un palier donné, comme décidé au point 5. Une compétence « acquise » dont le délai est dépassé sans nouvelle tentative passe à « à réviser » via `ReviewScheduler.mark_overdue_as_review`, à appeler par `ProgressManager` en début de séance.
## 13. Étape « Profils et sauvegardes » : précisions prises en cours d'implémentation
- Pas de classes de modèle dans `scripts/models/` (présentes dans l'arborescence indicative du §7) : les sauvegardes restent des `Dictionary` validés contre `data/schemas/save.schema.json`, comme les autres données. Ajout à la liste des écarts du point 9 ci-dessus.
- Découpage en couches : `scripts/profiles/save_io.gd` (lecture/écriture atomique, copie de secours, pas pur — touche le système de fichiers), `save_migration.gd` et `save_factory.gd` (purs), `progress_updates.gd` (pur, s'appuie sur `ReviewScheduler`/`DifficultyManager`). Les autoloads `SaveManager`, `ProfileManager`, `ProgressManager` restent de la colle fine : aucune logique propre, seulement de l'orchestration et de l'état en mémoire (profil courant, sauvegarde courante).
- `id` de profil généré par `ProfileManager` : `profile_<epoch>_<alea>` (conforme au motif `^[a-z][a-z0-9_]*$` du schéma). Pas de saisie de pseudonyme distinct de `display_name` à ce stade — à revoir si besoin à l'étape « Scènes ».
- Génie du moteur JSON de Godot : les nombres relus depuis un fichier sont toujours des `float`, jamais des `int`, même pour un entier comme `session_count`. GDScript convertit implicitement à l'affectation dans une variable typée `int`, mais les comparaisons strictes dans les tests doivent caster explicitement (`int(...)`), sinon GUT émet un avertissement.
- `JSON.parse_string` sur un contenu invalide imprime une erreur moteur (`Parse JSON failed`), même quand l'appelant la gère correctement via la valeur de retour. GUT la compte comme un échec de test par défaut : les tests qui provoquent volontairement cette corruption doivent appeler `assert_engine_error("Parse JSON failed")` pour la marquer comme attendue.
- Les indicateurs « erreurs fréquentes » et l'espace adulte (§9) ne sont pas implémentés : explicitement hors périmètre de la première version jouable (prompt de lancement, Objectif).