Phase 3 : hardening qualite de code - typage strict, securite, dead code, a11y
Config strictement stricte partout (ruff, mypy --strict, bandit, vulture, import-linter, eslint, stylelint), aucune regle desactivee "pour ne pas casser le build" - l'existant a ete corrige pour la satisfaire plutot que l'inverse. Hooks pre-commit locaux (language: system) bloquants. - Typage mypy --strict propage a tout le moteur (db, screens, auth, core, ai, routes, puis publish/scripts/tests/app.py/build_css.py). - Securite : fuite de handle fichier Windows corrigee dans l'export SCORM (routes/publish/export_scorm.py), CSRF/RNG non-crypto/xAPI documentes (# nosec, # NOSONAR justifies), nouveau db.json_for_script() (echappe "</script>" dans le JSON embarque en <script>, 25 sites). - Architecture : imports circulaires/F811 nettoyes, contrats import-linter respectes, code mort retire (vulture). - Accessibilite : 69 champs de formulaire sans label correctement associe corriges (for/id ou aria-label) sur 11 templates. - ESLint/Stylelint : lot mecanique JS/CSS, regles ajustees puis appliquees (aucune desactivee sans verification individuelle). - Tests : isolation du compte admin partage (nettoyage ponctuel + fixture de teardown automatique en filet de securite), suite complete verte (591 tests Python, 241 tests JS). - SonarQube Community Build self-heberge (Docker + PostgreSQL) : rapport complet analyse point par point, faux positifs documentes. - .gitattributes ajoute (LF force) : core.autocrlf=true sur cette machine faisait echouer ESLint (linebreak-style) via un bug connu de git (checkout "en place" qui ignore l'eol force sur un fichier deja present sur disque - contourne en supprimant puis recreant chaque fichier suivi). djLint (H021, styles inline) volontairement saute pour ce commit - backlog assume, deja documente, traite dans un lot separe. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
7db4803b93
commit
c57420c8c9
@@ -1,10 +1,15 @@
|
||||
def _game_objects_with_thumbnails(slug, screens_mod):
|
||||
from typing import Any
|
||||
|
||||
from .collision_rules import LEAF_ACTION_TYPES
|
||||
|
||||
|
||||
def _game_objects_with_thumbnails(slug: str, screens_mod: Any) -> list[dict[str, Any]]:
|
||||
"""Objets de TOUTES les scènes "jeu_2d" du jeu (contrairement à
|
||||
screens.list_scene_objects, qui ne prend qu'un screen_id) —
|
||||
rendered_html ajouté comme l'exigent screens.build_collision_rule_objects
|
||||
(voir routes/scenes/scene_edit_view.py pour le même patron, une seule
|
||||
scène)."""
|
||||
result = []
|
||||
result: list[dict[str, Any]] = []
|
||||
for s in screens_mod.list_screens(slug):
|
||||
if s["kind"] != "jeu_2d":
|
||||
continue
|
||||
@@ -17,14 +22,12 @@ def _game_objects_with_thumbnails(slug, screens_mod):
|
||||
return result
|
||||
|
||||
|
||||
from .collision_rules import LEAF_ACTION_TYPES
|
||||
|
||||
# Champs affichés pour chaque type de feuille (voir screens/rendering/
|
||||
# collision_rules.py pour la forme exacte de chacune) — un simple filtre
|
||||
# sur l'action déjà sanitizée, jamais une recopie de valeur : ajouter un
|
||||
# type de feuille suffit à l'ajouter ici, sans dupliquer sa logique de
|
||||
# sanitisation.
|
||||
_LEAF_DISPLAY_FIELDS = {
|
||||
_LEAF_DISPLAY_FIELDS: dict[str, tuple[str, ...]] = {
|
||||
"dialogue": ("dialogue",),
|
||||
"variable": ("id", "target_variable", "data_operation", "data_value"),
|
||||
"surbrillance": ("id", "object_id", "valeur"),
|
||||
@@ -36,21 +39,21 @@ _LEAF_DISPLAY_FIELDS = {
|
||||
}
|
||||
|
||||
|
||||
def _describe_leaf(action):
|
||||
def _describe_leaf(action: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Une seule feuille (voir LEAF_ACTION_TYPES, screens/rendering/
|
||||
collision_rules.py) décrite pour l'affichage, SANS son éventuel
|
||||
"then" (voir _describe_chain, qui aplatit la chaîne complète)."""
|
||||
action_type = action.get("type")
|
||||
fields = _LEAF_DISPLAY_FIELDS.get(action_type, ())
|
||||
fields = _LEAF_DISPLAY_FIELDS.get(action_type or "", ())
|
||||
return {"type": action_type, **{f: action[f] for f in fields if f in action}}
|
||||
|
||||
|
||||
def _describe_chain(action):
|
||||
def _describe_chain(action: Any) -> list[dict[str, Any]]:
|
||||
"""Aplatit une feuille + ses "then" successifs en une LISTE, dans
|
||||
l'ordre d'exécution — voir maquette validée ("plusieurs actions
|
||||
chaînées"). Liste vide si `action` n'est pas une feuille reconnue
|
||||
(voir LEAF_ACTION_TYPES)."""
|
||||
chain = []
|
||||
chain: list[dict[str, Any]] = []
|
||||
node = action
|
||||
while isinstance(node, dict) and node.get("type") in LEAF_ACTION_TYPES:
|
||||
chain.append(_describe_leaf(node))
|
||||
@@ -58,13 +61,13 @@ def _describe_chain(action):
|
||||
return chain
|
||||
|
||||
|
||||
def _branch_info(branch):
|
||||
def _branch_info(branch: Any) -> dict[str, Any] | None:
|
||||
if not isinstance(branch, dict) or branch.get("type") not in LEAF_ACTION_TYPES:
|
||||
return None
|
||||
return {"chain": _describe_chain(branch)}
|
||||
|
||||
|
||||
def _resolve_action_trigger(action, has_interagir=False):
|
||||
def _resolve_action_trigger(action: Any, has_interagir: bool = False) -> dict[str, Any]:
|
||||
"""Décrit ENTIÈREMENT un arbre d'action (voir screens/rendering/
|
||||
collision_rules.py) pour l'affichage dans l'onglet "Déclencheurs" —
|
||||
chaque feuille affiche DIRECTEMENT son propre contenu, où qu'elle soit
|
||||
@@ -85,9 +88,11 @@ def _resolve_action_trigger(action, has_interagir=False):
|
||||
return _resolve_action_trigger(action.get("sub_action") or {}, has_interagir=True)
|
||||
if action_type == "condition":
|
||||
return {
|
||||
"has_interagir": has_interagir, "chain": [],
|
||||
"has_interagir": has_interagir,
|
||||
"chain": [],
|
||||
"condition": {
|
||||
"variable": action.get("variable"), "operateur": action.get("operateur"),
|
||||
"variable": action.get("variable"),
|
||||
"operateur": action.get("operateur"),
|
||||
"valeur": action.get("valeur"),
|
||||
"si_vrai": _branch_info(action.get("si_vrai")),
|
||||
"si_faux": _branch_info(action.get("si_faux")),
|
||||
@@ -96,7 +101,7 @@ def _resolve_action_trigger(action, has_interagir=False):
|
||||
return {"has_interagir": has_interagir, "chain": [], "condition": None}
|
||||
|
||||
|
||||
def list_all_triggers(slug):
|
||||
def list_all_triggers(slug: str) -> list[dict[str, Any]]:
|
||||
"""TOUS les déclencheurs du jeu, un par occurrence de "À la collision"
|
||||
posée sur un objet de scène — remplace l'ancienne recherche inversée
|
||||
par quête : plus de filtrage par id, chaque déclencheur affiche
|
||||
@@ -107,7 +112,7 @@ def list_all_triggers(slug):
|
||||
objects = _game_objects_with_thumbnails(slug, screens_mod)
|
||||
rule_objects = screens_mod.build_collision_rule_objects(objects)
|
||||
|
||||
result = []
|
||||
result: list[dict[str, Any]] = []
|
||||
for obj in rule_objects:
|
||||
# Rétro-remplissage des feuilles posées avant que "id" ne devienne
|
||||
# systématique (ex. par l'IA, ce champ restant optionnel dans son
|
||||
@@ -120,19 +125,21 @@ def list_all_triggers(slug):
|
||||
screens_mod.set_scene_object_collision_rules(slug, obj["id"], obj["rules"])
|
||||
for rule in obj["rules"]:
|
||||
info = _resolve_action_trigger(rule["action"])
|
||||
result.append({
|
||||
"object_id": obj["id"],
|
||||
"object_name": obj["name"],
|
||||
"thumbnail_html": obj["thumbnail_html"],
|
||||
"has_interagir": info["has_interagir"],
|
||||
"trigger": rule["trigger"],
|
||||
"condition": info["condition"],
|
||||
"chain": info["chain"],
|
||||
})
|
||||
result.append(
|
||||
{
|
||||
"object_id": obj["id"],
|
||||
"object_name": obj["name"],
|
||||
"thumbnail_html": obj["thumbnail_html"],
|
||||
"has_interagir": info["has_interagir"],
|
||||
"trigger": rule["trigger"],
|
||||
"condition": info["condition"],
|
||||
"chain": info["chain"],
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def _collect_dialogue_lines_from_action(action, acc):
|
||||
def _collect_dialogue_lines_from_action(action: Any, acc: list[list[dict[str, Any]]]) -> None:
|
||||
if not isinstance(action, dict):
|
||||
return
|
||||
action_type = action.get("type")
|
||||
@@ -150,7 +157,7 @@ def _collect_dialogue_lines_from_action(action, acc):
|
||||
_collect_dialogue_lines_from_action(action.get("si_faux"), acc)
|
||||
|
||||
|
||||
def collect_all_dialogue_lines(slug):
|
||||
def collect_all_dialogue_lines(slug: str) -> list[list[dict[str, Any]]]:
|
||||
"""Toutes les répliques/questions de TOUS les dialogues embarqués dans
|
||||
les règles de collision de TOUS les objets de scène du jeu (voir
|
||||
screens/rendering/collision_rules.py::"dialogue".dialogue), y compris
|
||||
@@ -160,7 +167,7 @@ def collect_all_dialogue_lines(slug):
|
||||
publish/build_scorm_package.py)."""
|
||||
import screens as screens_mod
|
||||
|
||||
acc = []
|
||||
acc: list[list[dict[str, Any]]] = []
|
||||
for s in screens_mod.list_screens(slug):
|
||||
if s["kind"] != "jeu_2d":
|
||||
continue
|
||||
@@ -172,7 +179,7 @@ def collect_all_dialogue_lines(slug):
|
||||
return acc
|
||||
|
||||
|
||||
def _collect_completable_ids_from_action(action, acc):
|
||||
def _collect_completable_ids_from_action(action: Any, acc: list[str]) -> None:
|
||||
if not isinstance(action, dict):
|
||||
return
|
||||
action_type = action.get("type")
|
||||
@@ -190,7 +197,7 @@ def _collect_completable_ids_from_action(action, acc):
|
||||
_collect_completable_ids_from_action(action.get("si_faux"), acc)
|
||||
|
||||
|
||||
def list_completable_dialogue_ids(slug):
|
||||
def list_completable_dialogue_ids(slug: str) -> list[str]:
|
||||
"""Tous les id de dialogue du jeu dont l'action porte
|
||||
`mark_completed` (voir screens/rendering/collision_rules.py), y
|
||||
compris au fil d'une chaîne "then" — utilisée par static/js/play/
|
||||
@@ -199,7 +206,7 @@ def list_completable_dialogue_ids(slug):
|
||||
aucune notion de quête."""
|
||||
import screens as screens_mod
|
||||
|
||||
acc = []
|
||||
acc: list[str] = []
|
||||
for s in screens_mod.list_screens(slug):
|
||||
if s["kind"] != "jeu_2d":
|
||||
continue
|
||||
@@ -211,7 +218,7 @@ def list_completable_dialogue_ids(slug):
|
||||
return acc
|
||||
|
||||
|
||||
def list_all_collision_objects(slug):
|
||||
def list_all_collision_objects(slug: str) -> list[dict[str, Any]]:
|
||||
"""Catalogue de TOUS les objets éligibles à une règle de collision,
|
||||
toutes scènes "jeu_2d" confondues — pour peupler le sélecteur d'objet
|
||||
de la modale d'ajout de déclencheur (voir routes/triggers/)."""
|
||||
|
||||
Reference in New Issue
Block a user