Files
Forge-Engine/docs/SESSION_RECAP.md
T
williamandClaude Sonnet 5 66d8eaeae8 Lots 1-3 modernisation JS (S8786/S2703/S2486) + retrait Sonar CI/prod
- 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>
2026-09-18 08:46:24 +02:00

243 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 `</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)
utilisaient `json.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.py` servait le zip exporté via
`send_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
un `contextlib.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 fixture `autouse=True` dans
`tests/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.py`
- `db/rows/relation_options.py`, `rows_referencing.py`
- `screens/clause_list_codec.py`
- `screens/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) — rejoue `ruff check`/`ruff format
--check`/`mypy .`/`vulture`/`bandit`/`lint-imports` dans une image
Docker jetable (mêmes commandes que `.pre-commit-config.yaml`, jamais
dupliquées avec des paramètres différents). `djlint` volontairement
exclu (backlog H021, voir §8).
- **`lint-js`** (bloquant) — `npm run lint:js` / `npm run lint:css` dans
une image `node:20-slim`.
- **`sonarqube`** (`continue-on-error: true`, non-bloquant) — scan
contre l'instance self-hébergée `sonar.forgebase.fr`. N'est **pas**
dans le `needs` de `build-and-push` : un échec ici n'affecte jamais le
reste du pipeline.
- **`build-and-push`** dépend désormais de `test-python`, `test-js`,
`lint-python` **et** `lint-js`.
- **`deploy`** sécurisé : le token du registre passe par
`--password-stdin` (jamais en argument `-p`, visible via `ps` sur 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'endpoint `sonar.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), retirer `continue-on-error: true` du job `sonarqube`
et l'ajouter au `needs` de `build-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).