- 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 <noreply@anthropic.com>
15 KiB
15 KiB
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/S106désactivées pourtests/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 cartests/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 destrict, 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 unmin_confidenceabaissé 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 importeroutes/core, jamais l'inverse.
ESLint (.eslintrc.json)
- Base
airbnb-base+ pluginunused-imports. max-lenporté à 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-varsavec une liste blanche (varsIgnorePattern) d'environ 95 noms : fonctions invoquées uniquement depuis des attributsonclick/onchangeinline dans les templates Jinja, invisibles pour l'analyse statique d'ESLint.- Bloc
globalsdocumentant les variables cross-fichiers volontaires (ex.gameData,screensData— voir section 5).
Stylelint (.stylelintrc.json)
- Base
stylelint-config-standard. selector-class-pattern/selector-id-patterndé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-specificitydé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(jobsonarqube), non-bloquant (continue-on-error: true) le temps que le rapport soit entièrement trié.
3. Lancer les checks en local
# 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 <chemin-absolu-du-repo>:/usr/src \
sonarsource/sonar-scanner-cli \
-Dsonar.host.url=http://sonarqube:9000 \
-Dsonar.token=<votre-token>
4. Procédure de justification d'une exception
- 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).
- 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).
- 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").
- Format attendu par outil (vérifié empiriquement cette session, chaque outil a ses propres exigences de position du mot-clé) :
- Bandit :
# nosec <CODE>— le motnosecdoit être immédiatement précédé d'un#sur la ligne. - Ruff :
# noqa: <CODE>— le motnoqadoit ê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 <règle> - raison, sur la ligne exacte. - SonarQube (JS) :
// NOSONAR <règle> - 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<script>, bonne position de ligne) — voir la limitation documentée en section 5 (Web:S5247).
- Bandit :
- Toute exception validée est répercutée ici (section 5), avec la date/le contexte. Une exception non documentée ici n'est pas considérée comme validée.
- Qui valide : aucune exception n'est appliquée sans validation explicite de l'utilisateur — présenter le choix avec ses compromis, jamais trancher seul quand plusieurs options légitimes existent.
5. Exceptions et faux positifs documentés
| Site(s) | Outil / règle | Raison | Contexte |
|---|---|---|---|
core/flask_app.py:22 |
python:S4502 (Sonar) |
CSRF géré par core/csrf_guard.py — garde maison globale (@app.before_request), testée dans test_csrf.py, jamais Flask-WTF. Sonar ne reconnaît pas cette implémentation custom. |
Phase 3 |
screens/data_actions/compute_operation.py (×2), static/js/play/offline/compute-operation.js (×2), static/js/scenes/collision-rules-editor.js (×2), static/js/triggers/trigger-editor.js |
B311/S311/python:S2245/javascript:S2245 |
Tirage aléatoire de jeu (dé, id local d'UI) — jamais un usage cryptographique. | Phase 3 |
static/js/play/offline/xapi-client.js (18 sites) + static/js/play/offline/__tests__/xapi-client.test.js (2 sites) |
javascript:S5332 |
Identifiants du vocabulaire xAPI standard ADL (http://adlnet.gov/expapi/...), jamais déréférencés en réseau — simples chaînes comparées/embarquées, le http:// fait partie du texte fixé par la spec. Le vrai endpoint réseau (config.endpoint) est toujours saisi par le créateur, jamais un littéral de ce fichier. |
Phase 3 |
publish/scorm_manifest.py |
B406 (Bandit) |
Seul fichier du dépôt qui touche du XML — uniquement en génération (xml.sax.saxutils.escape), jamais en parsing d'XML externe. |
Phase 3 |
scripts/build_demo_dialogues.py:61-63 |
python:S8371 (Sonar) |
Accès direct resp.headers["Location"] volontaire : script d'usage unique jamais exécuté en production, un KeyError cru est un échec au moins aussi clair qu'un .get() renvoyant None. |
Phase 3 |
static/js/scenes/collision-rules-editor.js (leafAction.then = nextLeaf) |
javascript:S7739 |
then est un champ métier ("action suivante de la chaîne"), jamais une promesse — vérifié qu'aucun site d'appel ne le passe à await/Promise.resolve(). Risque latent documenté plutôt que renommage (le nom est ancré dans le schéma JSON persisté en base et côté Python). |
Phase 3 |
db/assert_not_none.py:14 |
B101/S101/python:S7632 |
Unique assert de narrowing de type restant dans tout le moteur, après centralisation de 11 sites dispersés (ai/, routes/scenes/, screens/payload/, scripts/) dans ce helper unique. Voir section 4 pour l'impossibilité structurelle de satisfaire Bandit+Ruff avec un seul #. |
Session du 16/09/2026 |
db/rows/delete_row.py, db/rows/get_row.py, db/rows/list_rows.py, screens/animations/update_animation_clip.py, screens/flow/add_flow_node.py, screens/scenes/add_scene_object.py |
B608/S608/python:S7632 |
Noms de table/colonnes construits uniquement à partir de slugify()/whitelists codées en dur (_UPDATABLE_FIELDS, FLOW_NODE_FIELDS), jamais d'une entrée arbitraire — valeurs toujours paramétrées (?). Distinct du cas assert_not_none ci-dessus (nature différente : construction de SQL, pas narrowing de type) — non couvert par ce refactor. |
Session du 16/09/2026 |
static/js/play/bindings.js:179 (gameData = newData) |
javascript:S2703 |
Pattern volontaire de scripts globaux (pas des modules ES) : gameData/screensData sont déclarés une fois par let dans le <script> inline de templates/play.html, chargé avant tous les static/js/play/*.js (ordre séquentiel vérifié, pas de defer/async). Réassignation légitime d'une variable déjà déclarée dans un scope global partagé — déjà documenté dans .eslintrc.json ("gameData": "writable"). |
Session du 16/09/2026 |
31 sites |safe (templates/scene_edit.html, templates/play.html, templates/auth/register_2fa.html, templates/onboarding/onboarding_new.html, templates/game_dashboard_simple.html) |
Web:S5247 (Sonar) |
Faux positif confirmé sur le fond, mais non-supprimable techniquement pour l'instant. 3 sous-groupes : (1) rendered_html/qr_svg/description — HTML déjà échappé côté Python (html.escape()) ou généré sans texte libre utilisateur ; (2) 25 sites *_json — db.json_for_script() échappe déjà </script> (voir Phase 3). Plusieurs syntaxes de suppression testées (commentaire Jinja {# #}, commentaire JS natif dans un <script>, bonne position de ligne) : aucune ne fonctionne avec l'analyseur Web de cette version de SonarQube. La résolution "Faux positif" via l'API est bloquée par le système de permissions de session. sonar.issue.ignore.multicriteria existe mais sans sélecteur de ligne (exclusion fichier entier uniquement) — écarté pour scene_edit.html/play.html (masquerait un futur |safe réellement dangereux). Ces 31 sites restent visibles dans le rapport Sonar en l'état ; traités et compris, pas un point ouvert côté code. |
Phase 3 + investigation du 16/09/2026 |
6. Backlog qualité
- djLint H021 (49 occurrences, 6 templates) — styles inline à remplacer par un système de modèles/classes CSS réutilisables. Priorité immédiate après la clôture de ce dispositif, avant ou après la Phase 4 CI selon décision à prendre le moment venu. Ce n'est pas une exception documentée en section 5 : c'est une dette assumée et non traitée, à corriger, pas à justifier indéfiniment.
- Code smells SonarQube (421 restants) — analysés en volume (modernisation JS, complexité cognitive Python, duplication) mais pas encore triés catégorie par catégorie. Prochain lot après ce document.
- Trajectoire SonarQube CI bloquant — une fois (a) le rapport de code smells trié et (b) une solution trouvée pour
Web:S5247(ou acceptée comme limitation permanente), retirercontinue-on-error: truedu jobsonarqubedans.gitea/workflows/deploy.yml.