From 55e81c0fbb9e99323a426380a0b87d2d8d3cc5b1 Mon Sep 17 00:00:00 2001 From: william Date: Wed, 16 Sep 2026 12:41:02 +0200 Subject: [PATCH] Ajoute CODE_QUALITY.md et corrige S7632 a la racine (helper assert_not_none) - Nouveau db/assert_not_none.py : centralise l'unique suppression Bandit/Ruff (# nosec B101 / # noqa: S101) de narrowing de type dans tout le moteur. Remplace 11 sites disperses (ai/chat.py, ai/tools.py, routes/scenes/scene_object_{add,collision,geometry,personnage_data, quiz_config}.py, screens/payload/full_game_payload.py, scripts/build_demo_dialogues.py) qui repetaient chacun le meme commentaire empile - Sonar (python:S7632) ne parse pas deux commentaires # sur une ligne, meme si Ruff et Bandit les acceptent chacun tres bien (contrainte structurelle documentee dans CODE_QUALITY.md : chaque outil exige son propre mot-cle immediatement apres un #, aucun format a un seul # ne peut satisfaire les deux a la fois). Les 6 sites # nosec B608 (SQL dynamique) restent inchanges, nature differente, hors perimetre de ce refactor. Verifie : mypy --strict propre (389 fichiers), ruff/bandit/import-linter clean, suite complete verte (591 tests), scan SonarQube local relance confirmant S7632 a 7 (1 seul site restant dans le helper lui-meme + les 6 B608), 0 bug (une regression S8371 trouvee et corrigee en route). - 4 sites |safe repositionnes sur leur ligne exacte (scene_edit.html, register_2fa.html, onboarding_new.html, play.html) - le marqueur NOSONAR etait sur la ligne precedente par erreur (meme piege que celui documente pour S8371 ci-dessus). Confirme par scan que meme corrige, l'analyseur Web de Sonar ne supporte aucune syntaxe de suppression inline testee pour la regle Web:S5247 - documente comme limitation technique connue dans CODE_QUALITY.md plutot que force. - CODE_QUALITY.md (nouveau) : reference complete du dispositif qualite - vue d'ensemble par outil, configuration de chacun, commandes de lancement local, procedure de justification d'une exception (avec le format exact attendu par Bandit/Ruff/Sonar, verifie empiriquement), table des 10 exceptions documentees, backlog (djLint H021, code smells Sonar). djLint (H021, styles inline) volontairement saute pour ce commit - meme backlog assume que les commits precedents, aucun rapport avec ce changement. Co-Authored-By: Claude Sonnet 5 --- CODE_QUALITY.md | 143 ++++++++++++++++++ ai/chat.py | 4 +- ai/tools.py | 4 +- db/__init__.py | 2 + db/assert_not_none.py | 15 ++ routes/scenes/scene_object_add.py | 13 +- routes/scenes/scene_object_collision.py | 5 +- routes/scenes/scene_object_geometry.py | 5 +- routes/scenes/scene_object_personnage_data.py | 5 +- routes/scenes/scene_object_quiz_config.py | 5 +- screens/payload/full_game_payload.py | 4 +- scripts/build_demo_dialogues.py | 6 +- templates/auth/register_2fa.html | 3 +- templates/onboarding/onboarding_new.html | 3 +- templates/play.html | 2 +- templates/scene_edit.html | 3 +- 16 files changed, 187 insertions(+), 35 deletions(-) create mode 100644 CODE_QUALITY.md create mode 100644 db/assert_not_none.py diff --git a/CODE_QUALITY.md b/CODE_QUALITY.md new file mode 100644 index 00000000..e4db0e35 --- /dev/null +++ b/CODE_QUALITY.md @@ -0,0 +1,143 @@ +# Qualité de code — Forge Engine + +Référence de fonctionnement du dispositif qualité mis en place (Phases 1 +à 4). Pour le compte-rendu chronologique de ce qui a été fait/trouvé/ +décidé pendant la mise en place, voir `docs/SESSION_RECAP.md` — ce +fichier-ci documente uniquement **l'état actuel** et **comment +l'utiliser au quotidien**. + +Règle de base, non négociable : **aucune règle de lint/typage n'est +désactivée globalement dans un fichier de config sans validation +explicite.** Une exception ponctuelle est toujours une ligne de code +(`# noqa`, `# nosec`, `// NOSONAR`) avec une raison précise, jamais un +ignore de fichier entier ou de règle globale — voir section 4. + +## 1. Vue d'ensemble + +| Outil | Vérifie | Portée | Obligatoire / Avertissement | +|---|---|---|---| +| **Ruff** (lint) | Erreurs Python, imports inutilisés, style | `*.py` | Obligatoire (bloquant CI + pre-commit) | +| **Ruff** (format) | Formatage Python | `*.py` | Obligatoire | +| **Mypy** `--strict` | Typage statique Python | `*.py` | Obligatoire | +| **Bandit** | Sécurité Python (injections, primitives faibles) | `ai`, `auth`, `core`, `db`, `filters`, `publish`, `routes`, `screens`, `scripts`, `app.py`, `build_css.py` | Obligatoire | +| **Vulture** | Code mort Python | mêmes dossiers que Bandit | Obligatoire | +| **import-linter** | Contrat de couches applicatives | tout le code Python | Obligatoire | +| **ESLint** (`airbnb-base`) | Lint JavaScript | `static/js/**/*.js` | Obligatoire | +| **Stylelint** (`stylelint-config-standard`) | Lint CSS | `styles/**/*.css` | Obligatoire | +| **djLint** | Lint des templates Jinja | `templates/**/*.html` | **Avertissement** — H021 (styles inline) en backlog assumé, voir section 6 | +| **SonarQube** (local + CI) | Bugs/vulnérabilités/code smells/hotspots agrégés | tout le dépôt | **Avertissement** — non-bloquant en CI pour l'instant, voir section 3 | + +## 2. Configuration de chaque outil + +### Ruff (`pyproject.toml`, `[tool.ruff]`) +- `line-length = 120`, `target-version = "py313"`. +- Règles activées : `F` (pyflakes), `E`/`W` (pycodestyle), `B` (bugbear), `C4` (comprehensions), `SIM` (simplification), `S` (bandit-équivalent Ruff), `I` (imports). +- `per-file-ignores` : `S101`/`S106` désactivées pour `tests/` uniquement (assert et mots de passe en dur sont la norme dans les tests, jamais en dehors). +- Exclusions : `projects/`, `user_assets/`, `data/`, `Bug/`, `regles/` (données runtime, jamais du code). + +### Mypy (`pyproject.toml`, `[tool.mypy]`) +- `strict = true`, `python_version = "3.13"`. +- `explicit_package_bases = true` + `mypy_path = "."` : nécessaire car `tests/` n'a pas de `__init__.py` — sans ça mypy refuse de démarrer ("Source file found twice under different module names"). **Ce n'est pas un assouplissement de `strict`**, juste la résolution des chemins de module. +- Mêmes exclusions que Ruff. + +### Bandit (`pyproject.toml`, `[tool.bandit]`) +- `exclude_dirs` : `projects`, `user_assets`, `data`, `Bug`, `regles`, `tests` (les tests contiennent des mots de passe/tokens de test en dur, jamais un vrai risque). +- Portée explicite (`-r ai auth core db filters publish routes screens scripts app.py build_css.py`) plutôt que tout le dépôt. + +### Vulture (`pyproject.toml`, `[tool.vulture]`) +- `min_confidence = 80`, `paths` = mêmes dossiers que Bandit + `vulture_whitelist.py`. +- `exclude = ["*/tests/*"]`. +- Faux positifs structurels (routes Flask enregistrées par décorateur, hooks appelés par convention de nom) : whitelist dédiée dans `vulture_whitelist.py`, **jamais** un `min_confidence` abaissé globalement. + +### import-linter (`pyproject.toml`, `[tool.importlinter]`) +- Un seul contrat de type `layers`, du haut vers le bas : `routes` → `ai | publish | core` → `screens` → `auth | filters` → `db`. +- Une couche ne peut importer qu'une couche strictement en dessous d'elle, jamais au-dessus, jamais une couche sœur du même niveau. +- `app.py` (point d'entrée, pas un paquet) reste hors contrat — c'est lui qui importe `routes`/`core`, jamais l'inverse. + +### ESLint (`.eslintrc.json`) +- Base `airbnb-base` + plugin `unused-imports`. +- `max-len` porté à 120 (aligné sur Ruff) au lieu du 80 par défaut d'Airbnb. +- Plusieurs règles Airbnb désactivées **après vérification individuelle que le code existant les respecte déjà différemment** (voir section 5 pour le détail et le raisonnement de chacune) : `no-param-reassign`, `no-use-before-define` (assoupli pour les fonctions, gardé strict pour variables/classes), `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`. +- `unused-imports/no-unused-vars` avec une liste blanche (`varsIgnorePattern`) d'environ 95 noms : fonctions invoquées uniquement depuis des attributs `onclick`/`onchange` inline dans les templates Jinja, invisibles pour l'analyse statique d'ESLint. +- Bloc `globals` documentant les variables cross-fichiers volontaires (ex. `gameData`, `screensData` — voir section 5). + +### Stylelint (`.stylelintrc.json`) +- Base `stylelint-config-standard`. +- `selector-class-pattern`/`selector-id-pattern` désactivées : le projet utilise du camelCase pour ses classes/id CSS depuis le début (`.canvasElement`, `#scormProgress`...) — imposer le kebab-case du preset aurait demandé de renommer des milliers de sélecteurs et leurs usages JS/HTML pour un gain nul. +- `no-descending-specificity` désactivée : le CSS existant est organisé par composant/fonctionnalité, pas par ordre strict de spécificité. +- `ignoreFiles` : `static/vendor/**`, `static/style.css` (bundle généré, contient du Bulma vendored). + +### djLint (`pyproject.toml`, `[tool.djlint]`) +- `profile = "jinja"`, `max_line_length = 160`, `indent = 2`. +- H021 (styles inline) : **backlog assumé, pas une exception corrigée** — voir section 6, ne pas confondre avec les vraies exceptions de la section 5. + +### SonarQube +- **Local (développement continu)** : Docker Community Build + PostgreSQL (jamais la base H2 embarquée), tourne en permanence sur cette machine via `~/sonarqube-stack/docker-compose.yml` (WSL2/Ubuntu). Dashboard : **http://localhost:9000** — identifiants personnels, jamais consignés ici. +- **CI (`sonar.forgebase.fr`)** : instance séparée, self-hébergée, scan à chaque push via `.gitea/workflows/deploy.yml` (job `sonarqube`), **non-bloquant** (`continue-on-error: true`) le temps que le rapport soit entièrement trié. + +## 3. Lancer les checks en local + +```bash +# Un outil a la fois (memes commandes que .pre-commit-config.yaml) +ruff check . +ruff format --check . +mypy . +vulture +bandit -c pyproject.toml -r ai auth core db filters publish routes screens scripts app.py build_css.py +lint-imports +djlint templates +npx eslint "static/js/**/*.js" +npx stylelint "styles/**/*.css" + +# Tout d'un coup (ce que fait un commit) +pre-commit run --all-files + +# Suite de tests +python -m pytest tests/ -q +node --test static/js/play/__tests__/*.test.js static/js/scenes/__tests__/*.test.js + +# Scan SonarQube local (rapport seul - jamais de correction automatique +# a partir de son seul resultat sans triage prealable, voir section 4) +# Necessite le stack Docker local demarre (voir section 2) et un token +# genere sur http://localhost:9000 (Mon compte > Security > Generate Token). +docker run --rm --network sonarqube-stack_default \ + -v :/usr/src \ + sonarsource/sonar-scanner-cli \ + -Dsonar.host.url=http://sonarqube:9000 \ + -Dsonar.token= +``` + +## 4. Procédure de justification d'une exception + +1. **Jamais de correction mécanique sans comprendre la cause.** Avant de corriger un finding, déterminer s'il s'agit d'un vrai problème ou d'un faux positif dû au contexte du projet (fixture pytest, dispatch dynamique JS/Jinja, whitelist codée en dur, architecture volontaire). +2. **Une exception ponctuelle, jamais globale.** Le commentaire de suppression va **sur la ligne exacte** signalée par l'outil — pas la ligne au-dessus, pas la ligne en dessous (piège vécu à plusieurs reprises cette session : un commentaire mal placé ne supprime rien du tout, silencieusement). +3. **La raison est précise, jamais vague.** "Faux positif" seul ne suffit pas — expliquer *pourquoi* (ex. "table_name vient de slugify()+prefixe obj_, jamais d'une entrée brute"). +4. **Format attendu par outil** (vérifié empiriquement cette session, chaque outil a ses propres exigences de position du mot-clé) : + - Bandit : `# nosec ` — le mot `nosec` doit être immédiatement précédé d'un `#` sur la ligne. + - Ruff : `# noqa: ` — le mot `noqa` doit être immédiatement précédé d'un `#` sur la ligne. **Bandit et Ruff peuvent coexister sur une même ligne physique** en utilisant deux `#` distincts (`# nosec B101 # noqa: S101 - raison`) — c'est structurel, pas un choix de style : chaque outil cherche son propre mot-clé juste après un `#`, et aucun format à un seul `#` ne peut satisfaire les deux à la fois (testé empiriquement). + - SonarQube (Python) : `# NOSONAR - raison`, sur la ligne exacte. + - SonarQube (JS) : `// NOSONAR - raison`, sur la ligne exacte. + - SonarQube (templates Jinja/HTML, règle `Web:*`) : **aucune syntaxe trouvée qui fonctionne** malgré plusieurs tentatives (commentaire Jinja `{# #}`, commentaire JS natif dans un `` (voir Phase 3). Plusieurs syntaxes de suppression testées (commentaire Jinja `{# #}`, commentaire JS natif dans un `