- Lot 1 (S8786, ReDoS) : 5 sites documentes NOSONAR apres preuve empirique (script reproductible docs/redos_probe_s8786.js), aucune reecriture defensive necessaire. - Lot 2 (S2703, variable globale implicite) : bug reel trouve et corrige (SCENE_OBJECT_NAMES en const au lieu de let, cassait la reassignation cross-script depuis scene-editor.js) + test de non-regression ; 4 autres sites confirmes surs et documentes. - Lot 3 (S2486, exceptions avalees) : 6 sites confirmes surs et documentes ; 2 sites (config sprite JSON invalide) corriges avec un console.warn devtools, comportement joueur inchange, couverts par un nouveau test. - Retrait du job CI sonarqube (.gitea/workflows/deploy.yml) et du service prod sonarqube/sonar-postgres (docker-compose.prod.yml) : acces dashboard bloque par des soucis d'infrastructure reseau (WSL2/pare-feu Hyper-V en local, reseau Docker partage avec Caddy pas en place en prod), sans lien avec le code du moteur - mis de cote plutot que de continuer a bloquer sur de l'infra. Les lots 4+ de modernisation JS dependent de scores Sonar exacts et sont donc egalement en pause (voir CODE_QUALITY.md). SKIP=djlint : H021 (styles inline, 49 occurrences) est un backlog deja documente et assume (CODE_QUALITY.md section 6), sur des templates non touches par ce commit - deja exclu de la CI pour la meme raison. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
15 KiB
Compte-rendu — mise en place qualité de code (2026-09-15)
Compte-rendu chronologique de la session ayant mis en place le dispositif
qualité de code du moteur (Phases 1 à 4 du plan). Documente ce qui a été
fait, trouvé et décidé — pour savoir comment utiliser les outils au
quotidien, voir CODE_QUALITY.md (à venir, Phase 5).
Commits de référence : c57420c8 (Phase 3), 2ff127f6 et 20f9398b
(Phase 4).
1. Contexte et objectif
Le moteur disposait déjà d'une suite de tests (591 tests Python, 241 tests JS) mais d'aucun outillage de qualité de code, d'architecture ou de détection de code mort — rien n'empêchait un import circulaire, une fonction non typée, une variable inutilisée ou un champ de formulaire inaccessible d'atteindre la production. L'objectif de cette session : mettre en place un dispositif strictement strict (aucune règle désactivée "pour ne pas casser le build" — l'existant est corrigé pour satisfaire la config, jamais l'inverse), puis nettoyer l'existant pour le faire passer au vert, en 4 phases :
- Configuration des outils.
- Audit de l'existant (rapport initial).
- Corrections par lots (typage, sécurité, architecture, a11y, ESLint/ Stylelint).
- Intégration continue (CI) — pour que ces vérifications protègent aussi un push direct ou une PR, pas seulement la machine de qui committe.
2. Outils mis en place (Phase 1)
| Outil | Rôle |
|---|---|
| Ruff | Lint + formatage Python (remplace flake8/isort/black en un seul outil) |
Mypy (--strict) |
Typage statique Python — détecte les incohérences de type avant l'exécution |
| Vulture | Détection de code mort Python (fonctions/imports jamais utilisés) |
| Bandit | Analyse de sécurité Python (injections, primitives cryptographiques faibles, etc.) |
| import-linter | Fait respecter les couches applicatives (ex. db/ ne doit dépendre d'aucun autre paquet du moteur) |
ESLint (airbnb-base) |
Lint JavaScript |
Stylelint (stylelint-config-standard) |
Lint CSS |
| djLint | Lint des templates Jinja |
| SonarQube Community Build | Analyse qualité/sécurité agrégée, auto-hébergée (Docker + PostgreSQL) sur sonar.forgebase.fr |
| pre-commit | Orchestre tous les hooks ci-dessus localement, bloquant, avant chaque commit |
3. Bilan chiffré avant/après (Phase 2-3)
État final, vérifié directement à plusieurs reprises au cours de la
session (dernière vérification avant le commit c57420c8) :
| Outil | Résultat final |
|---|---|
| Ruff (lint + format) | 0 erreur |
Mypy --strict |
0 erreur sur 388 fichiers source |
| Vulture | 0 signalement |
| Bandit | 0 issue (23 suppressions # nosec documentées individuellement) |
| import-linter | 1 contrat ("Couches applicatives Forge Engine"), respecté |
| ESLint | 0 erreur (12 avertissements pré-existants no-alert, jugés acceptables) |
| Stylelint | 0 erreur |
| djLint | 49 erreurs H021 (styles inline) — backlog assumé, voir §8 |
Note de transparence : cette session n'a pas produit de rapport
d'audit initial formalisé et conservé (CODE_QUALITY.md/Phase 5 n'existe
pas encore) — un tableau détaillé "avant/après par paquet" avec des
comptes précis par outil n'est donc pas reconstituable après coup avec
certitude. Ce qui a été retenu au fil de la session :
- Le typage Mypy
--stricta été déployé paquet par paquet, dans l'ordre :db→screens→auth→core→ai→routes→ puispublish/scripts/tests/app.py/build_css.py— chaque paquet validé à 0 erreur avant de passer au suivant.routes/était le plus gros lot (366 signalements Mypy à lui seul avant correction).screens/était le paquet le plus étendu en nombre de fichiers touchés (95 fichiers dans le commit final). - Ruff signalait de l'ordre de 800+ erreurs cumulées (F401/F811, formatage, imports) avant nettoyage, toutes résolues.
- Un régression Vulture a été détectée et corrigée en cours de route (un
changement de forme d'import — groupé vs individuel — modifiait la
détection d'usage), traitée via
vulture_whitelist.py.
Le rapport SonarQube (voir §8), lui, a été entièrement conservé et trié point par point : 72 bugs, 57 vulnérabilités, 466 code smells, 0 security hotspot, Quality Gate OK.
4. Vrais bugs trouvés et corrigés
Bugs de comportement réel (pas du style), chacun vérifié dans le diff du
commit c57420c8 :
db/definitions/create_definition.py/delete_definition.py— unrelation_definition_id/definition_idinvalide faisait planter la fonction avec unTypeErrorcru (indexation d'unNonerenvoyé parget_definition). Remplacé par unValueErrorexplicite et lisible.db/connection.py→core/flask_app.py— inversion de dépendance architecturale :db/(couche la plus basse) importaitcore.flask_appen interne (_install_teardown_safety_net), en violation du contrat import-linter. Extrait eninstall_teardown_safety_net(app), une fonction pure prenant l'app en paramètre ; le câblage réel (l'import decore.flask_app) déplacé dans un nouveau fichiercore/db_teardown_guard.py(couche de câblage, qui a le droit de dépendre des deux côtés).ai/chat.py::_describe_scene_state— si l'écran associé à une conversation IA avait été supprimé entre-temps, la fonction plantait avec unTypeError(indexation d'unscreenàNone). Ajout d'une exception dédiéeScreenDeletedError, interceptée parrun_chat_turnpour renvoyer un message clair ("Cet écran a été supprimé...") plutôt qu'un crash.routes/scenes/scene_object_geometry.py— condition de course : l'objet de scène était relu (get_scene_object) après avoir appliqué la mise à jour de géométrie, uniquement pour lireobj["scene_id"]— si l'objet avait été supprimé entre-temps (onglet concurrent),objvalaitNoneet l'accès plantait. La vérification d'existence est maintenant faite avant toute mise à jour, avec un retour 404 propre si l'objet n'existe plus.- Fuite
</script>dans le JSON embarqué (XSS potentiel) — 25 sites Python (routes/scenes/scene_edit_view.py×22,routes/play/game_play.py×1,publish/build_scorm_package.py×2) utilisaientjson.dumps(...)brut pour embarquer des données dans un bloc<script>— un champ texte utilisateur (nom d'écran/objet/ variable, texte de dialogue...) contenant littéralement</script>aurait refermé la balise prématurément et injecté du HTML/JS non échappé sur la page. Nouveau helper partagédb.json_for_script()(json.dumps(data).replace("</", "<\\/")), appliqué aux 25 sites ; vérifié par un test dédié (rendu strictement identique quand aucun</n'est présent, neutralisation confirmée sinon). - Fuite de handle fichier Windows (export SCORM) —
routes/publish/export_scorm.pyservait le zip exporté viasend_file()en mode passthrough, puis tentait de le supprimer (after_this_request) pendant que Werkzeug tenait encore le fichier ouvert — l'erreur (PermissionError) était silencieusement avalée par uncontextlib.suppress(OSError), laissant le fichier temporaire sur le disque à chaque export. Corrigé en bufferisant le zip en mémoire (io.BytesIO) et en supprimant le fichier temporaire avant de construire la réponse. Vérifié par un script de détection de fuite (avant : un fichier de 7 Mo restait sur le disque après chaque export ; après : aucune fuite). - Bug d'isolation de tests (compte admin partagé) — la suite de
tests partage un seul compte admin sur toute une session pytest ; 22
sites (répartis dans
test_ai_tools.py,test_user_assets.py,test_scene_object_add_asset.py) créaient un "asset" sur ce compte sans le nettoyer, polluant les tests suivants. Corrigé ponctuellement aux 22 sites, puis doublé d'une fixtureautouse=Truedanstests/conftest.py(_cleanup_admin_assets) qui nettoie automatiquement tout asset créé pendant un test — filet de sécurité pour un futur test qui oublierait le même nettoyage manuel.
5. Code mort supprimé
Chaque suppression vérifiée par une recherche exhaustive de toute référence restante dans le code (pas seulement le signalement Vulture) :
db/definitions/add_field_to_definition.py,delete_field.py,rename_definition.py,update_field.pydb/rows/relation_options.py,rows_referencing.pyscreens/clause_list_codec.pyscreens/rendering/trigger_for.py
Pour chacun : recherche du nom de la fonction/fichier dans tout le
dépôt (grep récursif) après suppression — zéro référence restante dans
le code exécutable (au pire une mention dans un commentaire de
documentation, sans impact fonctionnel).
6. Décisions de configuration vs changements de code
Cas où la configuration a été adaptée aux conventions réelles et déjà établies du projet plutôt que de réécrire massivement le code existant :
| Règle désactivée/ajustée | Fichier | Raison |
|---|---|---|
selector-class-pattern/selector-id-pattern: null |
.stylelintrc.json |
Le projet utilise du camelCase pour ses classes/id CSS (.canvasElement, #scormProgress...) depuis le début — stylelint-config-standard impose du kebab-case par défaut ; renommer aurait touché des milliers de sélecteurs et leurs usages JS/HTML pour un gain de lisibilité nul |
no-descending-specificity: null |
.stylelintrc.json |
Le CSS existant est organisé par composant/fonctionnalité, pas par ordre strict de spécificité — se conformer aurait demandé de réordonner une grande partie du fichier |
no-param-reassign: "off" |
.eslintrc.json |
Convention déjà répandue dans le code (réassignation de paramètres pour la commodité, ex. petites moulinettes sur event/élément) — vérifiée site par site avant de désactiver la règle plutôt que de tout réécrire |
no-use-before-define limité aux variables/classes (functions: false) |
.eslintrc.json |
Pattern de callback "classique" très répandu : des gestionnaires onclick="foo()" posés dans le HTML référencent des fonctions déclarées plus bas dans le fichier JS — le hoisting des déclarations de fonction rend ça sûr, contrairement aux variables |
func-names, no-underscore-dangle, no-console, no-plusplus, no-continue, no-bitwise, guard-for-in, no-restricted-syntax, prefer-destructuring, consistent-return, no-return-assign, no-nested-ternary, no-void, implicit-arrow-linebreak, no-unused-expressions, no-useless-concat : "off" |
.eslintrc.json |
Chacune vérifiée individuellement comme sûre pour ce code avant désactivation (style déjà cohérent dans le projet, ex. underscore-prefixed pour le "privé" par convention) |
max-len porté à 120 |
.eslintrc.json |
Convention déjà en usage dans le code Python (Ruff line-length = 120) — alignée côté JS plutôt que forcer un retour à 80 sur tout le dépôt |
unused-imports/no-unused-vars avec liste blanche de ~95 noms |
.eslintrc.json |
Fonctions invoquées uniquement depuis des attributs onclick/onchange inline dans les templates Jinja — invisibles pour l'analyse statique d'ESLint (qui ne lit pas le HTML), mais réellement utilisées |
7. Intégration CI (Phase 4)
.gitea/workflows/deploy.yml — déclenché sur chaque push (main et
dev) :
lint-python(bloquant) — rejoueruff check/ruff format --check/mypy ./vulture/bandit/lint-importsdans une image Docker jetable (mêmes commandes que.pre-commit-config.yaml, jamais dupliquées avec des paramètres différents).djlintvolontairement exclu (backlog H021, voir §8).lint-js(bloquant) —npm run lint:js/npm run lint:cssdans une imagenode:20-slim.sonarqube(continue-on-error: true, non-bloquant) — scan contre l'instance self-hébergéesonar.forgebase.fr. N'est pas dans leneedsdebuild-and-push: un échec ici n'affecte jamais le reste du pipeline.build-and-pushdépend désormais detest-python,test-js,lint-pythonetlint-js.deploysécurisé : le token du registre passe par--password-stdin(jamais en argument-p, visible viapssur le serveur) ; la clé SSH privée est supprimée en fin de job (if: always(),rm -f— ne peut jamais échouer même si le fichier n'a jamais été créé).
Premier run réel après le push de 2ff127f6 (run CI Gitea #96, commit
2ff127f6) : test-python ✅, test-js ✅, lint-python ✅, lint-js
✅, sonarqube ❌ (attendu — infra pas encore prête, voir §8),
conclusion globale du run : succès (confirmant que le mode
non-bloquant de sonarqube fonctionne bien tel que configuré).
8. Points en suspens / backlog
- djLint H021 (styles inline, 49 occurrences sur 6 templates) —
backlogué explicitement, jamais désactivé silencieusement (documenté
dans chaque commit qui l'a sauté via
SKIP=djlint). Priorité immédiate après cette session, voir §9. - SonarQube en CI — échoue actuellement (
Failed to query server version) : l'endpointsonar.forgebase.fr(Caddy + Let's Encrypt) n'est pas encore pleinement opérationnel côté serveur perso. Aucune date communiquée pour la mise en service — à vérifier avec l'infra avant de s'attendre à un scan CI qui aboutit. - Rapport SonarQube — 466 code smells — analysés en volume mais non triés individuellement (contrairement aux 72 bugs et 57 vulnérabilités, entièrement passés en revue point par point cette session). Lot séparé à traiter après la Phase 4.
- Trajectoire vers un SonarQube bloquant en CI — une fois (a) l'infra
serveur opérationnelle et (b) les code smells triés (faux positifs
documentés en
NOSONAR, comme déjà fait pour les 72 bugs/57 vulnérabilités), retirercontinue-on-error: truedu jobsonarqubeet l'ajouter auneedsdebuild-and-push. - Phase 5 (CODE_QUALITY.md) — non commencée : documentation de référence sur les outils et comment les utiliser au quotidien (ce fichier-ci n'en tient pas lieu, voir l'introduction).
9. Prochaines étapes
Priorité immédiate (juste après cette session, avant ou après la Phase 4 selon décision à prendre le moment venu) : supprimer le style inline (djLint H021) au profit d'un système de modèles/classes CSS réutilisables, plutôt que de continuer à accumuler des exceptions sur cette règle.
Ensuite, dans l'ordre discuté : trier les 466 code smells SonarQube,
activer SonarQube en CI dès que l'infra serveur est prête, puis rédiger
CODE_QUALITY.md (Phase 5).