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:
william
2026-09-15 16:06:15 +02:00
co-authored by Claude Sonnet 5
parent 7db4803b93
commit c57420c8c9
454 changed files with 16448 additions and 6967 deletions
@@ -17,13 +17,17 @@ function loadAsScript(relativePath) {
function makeEl() {
const el = {
_html: '', style: {}, value: '',
_html: '',
style: {},
value: '',
classList: { add() {}, remove() {}, toggle() {} },
children: [],
appendChild(c) { this.children.push(c); },
querySelector() { return null; },
setAttribute() {}, getAttribute() { return null; },
addEventListener() {}, removeEventListener() {},
setAttribute() {},
getAttribute() { return null; },
addEventListener() {},
removeEventListener() {},
};
Object.defineProperty(el, 'innerHTML', {
get() { return this._html; },
@@ -90,7 +94,7 @@ test('assistant — une chaîne "attendre -> puis dialogue" sauvegarde bien LES
const saveCall = posted.find((p) => p.url === '/game/pytest_wizard/screens/7/triggers');
assert.ok(saveCall, 'la règle aurait dû être postée');
const action = saveCall.body.rule.action;
const { action } = saveCall.body.rule;
assert.equal(action.type, 'attendre');
assert.equal(action.data_value, '2');
assert.equal(action.then.type, 'dialogue');
@@ -114,7 +118,7 @@ test('assistant — une chaîne de 3 actions ("variable" -> "attendre" -> "dialo
global.collisionWizardChainDone('none');
const saveCall = posted.find((p) => p.url === '/game/pytest_wizard/screens/7/triggers');
const action = saveCall.body.rule.action;
const { action } = saveCall.body.rule;
assert.equal(action.type, 'variable');
assert.equal(action.then.type, 'attendre');
assert.equal(action.then.data_value, '1.5');
@@ -131,7 +135,7 @@ test('assistant — une seule action ("Rien de plus" tout de suite) sauvegarde b
global.collisionWizardChainDone('none');
const saveCall = posted.find((p) => p.url === '/game/pytest_wizard/screens/7/triggers');
const action = saveCall.body.rule.action;
const { action } = saveCall.body.rule;
assert.equal(action.type, 'dialogue');
assert.equal(action.then, undefined);
});
@@ -142,9 +146,15 @@ test('assistant — l\'action "vidéo" ne propose QUE les fichiers vidéo (jamai
// USER_ASSETS_OPTIONS en entier, sans filtrer par kind.
setupGlobals();
global.USER_ASSETS_OPTIONS = [
{ id: 1, url: '/assets/1/photo.png', name: 'photo.png', kind: 'image' },
{ id: 2, url: '/assets/1/bip.mp3', name: 'bip.mp3', kind: 'audio' },
{ id: 3, url: '/assets/1/clip.mp4', name: 'clip.mp4', kind: 'video' },
{
id: 1, url: '/assets/1/photo.png', name: 'photo.png', kind: 'image',
},
{
id: 2, url: '/assets/1/bip.mp3', name: 'bip.mp3', kind: 'audio',
},
{
id: 3, url: '/assets/1/clip.mp4', name: 'clip.mp4', kind: 'video',
},
];
const body = document.getElementById('questTriggerModalBody');
@@ -161,9 +171,15 @@ test('assistant — l\'action "vidéo" ne propose QUE les fichiers vidéo (jamai
test('assistant — l\'action "son" ne propose QUE les fichiers audio (jamais les images/vidéos)', () => {
setupGlobals();
global.USER_ASSETS_OPTIONS = [
{ id: 1, url: '/assets/1/photo.png', name: 'photo.png', kind: 'image' },
{ id: 2, url: '/assets/1/bip.mp3', name: 'bip.mp3', kind: 'audio' },
{ id: 3, url: '/assets/1/clip.mp4', name: 'clip.mp4', kind: 'video' },
{
id: 1, url: '/assets/1/photo.png', name: 'photo.png', kind: 'image',
},
{
id: 2, url: '/assets/1/bip.mp3', name: 'bip.mp3', kind: 'audio',
},
{
id: 3, url: '/assets/1/clip.mp4', name: 'clip.mp4', kind: 'video',
},
];
const body = document.getElementById('questTriggerModalBody');
@@ -183,7 +199,9 @@ test('triggerActionChainHtml — un bouton "+ Ajouter une action" apparaît apr
// juste après lui, pas seulement le tout dernier de la chaîne.
setupGlobals();
const chain = [
{ type: 'variable', id: 'v_1', target_variable: 'score', data_operation: 'incrementer', data_value: '1' },
{
type: 'variable', id: 'v_1', target_variable: 'score', data_operation: 'incrementer', data_value: '1',
},
{ type: 'attendre', id: 'a_1', data_value: '2' },
{ type: 'dialogue', dialogue: { id: 'd_1', lines: [] } },
];