Files
williamandClaude Sonnet 5 c57420c8c9 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>
2026-09-15 16:06:15 +02:00

104 lines
4.0 KiB
Python

import os
from ..connection import connect
from ..game_dir import game_dir
from ..slugify import slugify
from .project_slug import build_slug
def create_game(name: str, owner_folder: str | None = None, project_slug_override: str | None = None) -> str:
"""Feature 1 : crée le dossier du jeu, ses fichiers index.html/css/js
reliés entre eux, et sa base de données dédiée (nom du jeu en méta).
owner_folder : dossier PROPRIÉTAIRE (voir db/games/project_slug.py —
structure de dossiers par utilisateur), l'id du compte en pratique
(voir routes/games/games_new.py — opaque, jamais dérivé de l'email) —
le slug final composé
"<owner_folder>_<project_part>" résout vers un vrai chemin imbriqué
projects/<owner_folder>/<project_part>/. Omis (None) : repli sur
l'ancien comportement plat (slug à un seul segment, projects/<slug>/)
— utile pour les tests de la couche données qui n'ont pas de compte
utilisateur à représenter.
project_slug_override : impose le nom du dossier PROJET au lieu de le
dériver de `name` — utilisé par l'inscription (auth/, voir
routes/onboarding/) pour que le tout premier projet d'un compte porte
un nom de dossier prévisible. Le suffixe d'unicité (-2/-3...) ne
compare que parmi les projets déjà présents dans CE `owner_folder`
(deux comptes différents peuvent donc avoir chacun un projet nommé
pareil, sans collision).
Un projet n'a plus de "type" propre (voir screens/screens_repo/
ensure_schema.py — fusion des moteurs, "document"/"jeu_2d" est
désormais une propriété PAR ÉCRAN) : un projet fraîchement créé n'a
par défaut aucun écran, c'est à l'appelant d'en créer un premier avec
le "kind" voulu (voir screens.create_screen)."""
project_part = slugify(project_slug_override) if project_slug_override else slugify(name)
base = project_part
n = 2
while os.path.isdir(game_dir(build_slug(owner_folder, base) if owner_folder else base)):
base = f"{project_part}-{n}"
n += 1
project_part = base
slug = build_slug(owner_folder, project_part) if owner_folder else project_part
os.makedirs(game_dir(slug), exist_ok=True)
index_html = f"""<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{name}</title>
<link rel="stylesheet" href="index.css">
</head>
<body>
<h1>{name}</h1>
<p>Squelette généré par le moteur Forge — à remplir au fur et à mesure
des fonctionnalités du jeu.</p>
<script src="index.js"></script>
</body>
</html>
"""
index_css = """body {
font-family: system-ui, sans-serif;
margin: 40px;
color: #222;
}
"""
index_js = """// index.js — point d'entrée du jeu, généré par le moteur Forge.
console.log("Jeu chargé.");
"""
with open(os.path.join(game_dir(slug), "index.html"), "w", encoding="utf-8") as f:
f.write(index_html)
with open(os.path.join(game_dir(slug), "index.css"), "w", encoding="utf-8") as f:
f.write(index_css)
with open(os.path.join(game_dir(slug), "index.js"), "w", encoding="utf-8") as f:
f.write(index_js)
conn = connect(slug)
conn.executescript(
"""
CREATE TABLE _meta (key TEXT PRIMARY KEY, value TEXT);
CREATE TABLE _definitions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
table_name TEXT NOT NULL UNIQUE,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE _fields (
id INTEGER PRIMARY KEY AUTOINCREMENT,
definition_id INTEGER NOT NULL REFERENCES _definitions(id) ON DELETE CASCADE,
name TEXT NOT NULL,
type TEXT NOT NULL,
relation_definition_id INTEGER REFERENCES _definitions(id),
required INTEGER NOT NULL DEFAULT 0,
position INTEGER NOT NULL DEFAULT 0
);
"""
)
conn.execute("INSERT INTO _meta (key, value) VALUES ('name', ?)", (name,))
conn.commit()
conn.close()
return slug