# 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 : 1. Configuration des outils. 2. Audit de l'existant (rapport initial). 3. Corrections par lots (typage, sécurité, architecture, a11y, ESLint/ Stylelint). 4. 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 `--strict` a été déployé **paquet par paquet**, dans l'ordre : `db` → `screens` → `auth` → `core` → `ai` → `routes` → puis `publish`/`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`** — un `relation_definition_id`/`definition_id` invalide faisait planter la fonction avec un `TypeError` cru (indexation d'un `None` renvoyé par `get_definition`). Remplacé par un `ValueError` explicite et lisible. - **`db/connection.py` → `core/flask_app.py`** — inversion de dépendance architecturale : `db/` (couche la plus basse) importait `core.flask_app` en interne (`_install_teardown_safety_net`), en violation du contrat import-linter. Extrait en `install_teardown_safety_net(app)`, une fonction pure prenant l'app en paramètre ; le câblage réel (l'import de `core.flask_app`) déplacé dans un nouveau fichier `core/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 un `TypeError` (indexation d'un `screen` à `None`). Ajout d'une exception dédiée `ScreenDeletedError`, interceptée par `run_chat_turn` pour 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 lire `obj["scene_id"]` — si l'objet avait été supprimé entre-temps (onglet concurrent), `obj` valait `None` et 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 `` 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) utilisaient `json.dumps(...)` brut pour embarquer des données dans un bloc `` 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("