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
@@ -18,24 +18,24 @@
|
||||
// full_game_payload.py) pour que conditions.js continue de la lire
|
||||
// exactement comme aujourd'hui.
|
||||
function forgeApplyVariableActionOffline(gameData, action) {
|
||||
var varName = action.target_variable;
|
||||
var operation = action.data_operation;
|
||||
var rawValue = action.data_value;
|
||||
const varName = action.target_variable;
|
||||
const operation = action.data_operation;
|
||||
const rawValue = action.data_value;
|
||||
if (!varName || !operation) return false;
|
||||
var variable = gameData.variables && gameData.variables[varName];
|
||||
const variable = gameData.variables && gameData.variables[varName];
|
||||
if (!variable) return false;
|
||||
var varType = variable.type;
|
||||
var current = variable.value;
|
||||
if (varType === "nombre_entier" || varType === "nombre_decimal") {
|
||||
current = (current === null || current === undefined || current === "") ? 0 : parseFloat(current);
|
||||
if (isNaN(current)) current = 0;
|
||||
} else if (varType === "booleen") {
|
||||
var normalized = String(current == null ? "" : current).trim().toLowerCase();
|
||||
current = (normalized === "1" || normalized === "true" || normalized === "vrai") ? 1 : 0;
|
||||
const varType = variable.type;
|
||||
let current = variable.value;
|
||||
if (varType === 'nombre_entier' || varType === 'nombre_decimal') {
|
||||
current = (current === null || current === undefined || current === '') ? 0 : parseFloat(current);
|
||||
if (Number.isNaN(current)) current = 0;
|
||||
} else if (varType === 'booleen') {
|
||||
const normalized = String(current == null ? '' : current).trim().toLowerCase();
|
||||
current = (normalized === '1' || normalized === 'true' || normalized === 'vrai') ? 1 : 0;
|
||||
}
|
||||
var newValue;
|
||||
let newValue;
|
||||
try {
|
||||
newValue = forgeComputeNewValue(operation, current, rawValue, varType === "nombre_decimal");
|
||||
newValue = forgeComputeNewValue(operation, current, rawValue, varType === 'nombre_decimal');
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
@@ -47,15 +47,15 @@ function forgeApplyVariableActionOffline(gameData, action) {
|
||||
// Port de apply_score_action.py/apply_status_action.py — voir db/scoring/
|
||||
// pour le vocabulaire des statuts (db.SCORE_STATUS_LABELS côté serveur,
|
||||
// dupliqué ici en dur : ces 5 clés sont figées, voir db/constants.py).
|
||||
var FORGE_SCORE_STATUS_KEYS = ["non_commence", "en_cours", "termine", "reussi", "echoue"];
|
||||
const FORGE_SCORE_STATUS_KEYS = ['non_commence', 'en_cours', 'termine', 'reussi', 'echoue'];
|
||||
|
||||
function forgeApplyScoreActionOffline(gameData, action) {
|
||||
var operation = action.data_operation;
|
||||
var rawValue = action.data_value;
|
||||
const operation = action.data_operation;
|
||||
const rawValue = action.data_value;
|
||||
if (!operation) return false;
|
||||
if (!gameData.scoring) gameData.scoring = { score: 0, status: "non_commence" };
|
||||
var current = gameData.scoring.score || 0;
|
||||
var newValue;
|
||||
if (!gameData.scoring) gameData.scoring = { score: 0, status: 'non_commence' };
|
||||
const current = gameData.scoring.score || 0;
|
||||
let newValue;
|
||||
try {
|
||||
newValue = forgeComputeNewValue(operation, current, rawValue, true);
|
||||
} catch (e) {
|
||||
@@ -65,16 +65,16 @@ function forgeApplyScoreActionOffline(gameData, action) {
|
||||
// xAPI bolt-on (voir static/js/play/offline/xapi-client.js) : no-op si
|
||||
// le fichier n'est pas chargé (tests Node via module.exports ci-dessous,
|
||||
// ou réglage xAPI jamais configuré pour ce jeu).
|
||||
if (typeof forgeXapiNotifyScoreChanged === "function") forgeXapiNotifyScoreChanged(newValue);
|
||||
if (typeof forgeXapiNotifyScoreChanged === 'function') forgeXapiNotifyScoreChanged(newValue);
|
||||
return true;
|
||||
}
|
||||
|
||||
function forgeApplyStatusActionOffline(gameData, action) {
|
||||
var status = action.data_value;
|
||||
const status = action.data_value;
|
||||
if (!status || FORGE_SCORE_STATUS_KEYS.indexOf(status) === -1) return false;
|
||||
if (!gameData.scoring) gameData.scoring = { score: 0, status: "non_commence" };
|
||||
if (!gameData.scoring) gameData.scoring = { score: 0, status: 'non_commence' };
|
||||
gameData.scoring.status = status;
|
||||
if (typeof forgeXapiNotifyStatusChanged === "function") forgeXapiNotifyStatusChanged(status);
|
||||
if (typeof forgeXapiNotifyStatusChanged === 'function') forgeXapiNotifyStatusChanged(status);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -86,45 +86,45 @@ function forgeApplyStatusActionOffline(gameData, action) {
|
||||
// {name, type, min_value, max_value} — même forme que le payload en
|
||||
// ligne (screens/payload/full_game_payload.py).
|
||||
function forgeFindFieldMeta(gameData, definitionId, fieldName) {
|
||||
var fields = gameData.fields_meta && gameData.fields_meta[definitionId];
|
||||
const fields = gameData.fields_meta && gameData.fields_meta[definitionId];
|
||||
if (!fields) return null;
|
||||
for (var i = 0; i < fields.length; i++) {
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
if (fields[i].name === fieldName) return fields[i];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function forgeClampToFieldBounds(value, fieldMeta) {
|
||||
if (fieldMeta.type !== "nombre_entier" && fieldMeta.type !== "nombre_decimal") return value;
|
||||
if (fieldMeta.type !== 'nombre_entier' && fieldMeta.type !== 'nombre_decimal') return value;
|
||||
if (fieldMeta.min_value !== null && fieldMeta.min_value !== undefined && value < fieldMeta.min_value) {
|
||||
value = fieldMeta.min_value;
|
||||
}
|
||||
if (fieldMeta.max_value !== null && fieldMeta.max_value !== undefined && value > fieldMeta.max_value) {
|
||||
value = fieldMeta.max_value;
|
||||
}
|
||||
return fieldMeta.type === "nombre_decimal" ? value : Math.trunc(value);
|
||||
return fieldMeta.type === 'nombre_decimal' ? value : Math.trunc(value);
|
||||
}
|
||||
|
||||
function forgeApplyDataActionOffline(gameData, action) {
|
||||
var definitionId = String(action.target_definition_id || "");
|
||||
var rowId = action.target_row_id;
|
||||
var fieldName = action.target_field;
|
||||
var operation = action.data_operation;
|
||||
var rawValue = action.data_value;
|
||||
const definitionId = String(action.target_definition_id || '');
|
||||
const rowId = action.target_row_id;
|
||||
const fieldName = action.target_field;
|
||||
const operation = action.data_operation;
|
||||
const rawValue = action.data_value;
|
||||
if (!(definitionId && rowId && fieldName && operation)) return false;
|
||||
var fieldMeta = forgeFindFieldMeta(gameData, definitionId, fieldName);
|
||||
const fieldMeta = forgeFindFieldMeta(gameData, definitionId, fieldName);
|
||||
if (!fieldMeta) return false;
|
||||
var rows = gameData.data && gameData.data[definitionId];
|
||||
const rows = gameData.data && gameData.data[definitionId];
|
||||
if (!rows) return false;
|
||||
var row = null;
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
let row = null;
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
if (rows[i].id === rowId) { row = rows[i]; break; }
|
||||
}
|
||||
if (!row) return false;
|
||||
var current = row[fieldName];
|
||||
var newValue;
|
||||
const current = row[fieldName];
|
||||
let newValue;
|
||||
try {
|
||||
newValue = forgeComputeNewValue(operation, current, rawValue, fieldMeta.type === "nombre_decimal");
|
||||
newValue = forgeComputeNewValue(operation, current, rawValue, fieldMeta.type === 'nombre_decimal');
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
@@ -140,17 +140,17 @@ function forgeApplyDataActionOffline(gameData, action) {
|
||||
// tout le reste -> null). L'id est un simple auto-incrément local (pas
|
||||
// de vraie base : aucune ligne d'un autre joueur à ignorer hors ligne).
|
||||
function forgeApplyAddRowActionOffline(gameData, action) {
|
||||
var definitionId = String(action.target_definition_id || "");
|
||||
const definitionId = String(action.target_definition_id || '');
|
||||
if (!definitionId) return null;
|
||||
var fields = gameData.fields_meta && gameData.fields_meta[definitionId];
|
||||
const fields = gameData.fields_meta && gameData.fields_meta[definitionId];
|
||||
if (!fields) return null;
|
||||
if (!gameData.data[definitionId]) gameData.data[definitionId] = [];
|
||||
var rows = gameData.data[definitionId];
|
||||
var maxId = 0;
|
||||
rows.forEach(function (r) { if (r.id > maxId) maxId = r.id; });
|
||||
var newRow = { id: maxId + 1 };
|
||||
fields.forEach(function (f) {
|
||||
newRow[f.name] = f.type === "booleen" ? 0 : null;
|
||||
const rows = gameData.data[definitionId];
|
||||
let maxId = 0;
|
||||
rows.forEach((r) => { if (r.id > maxId) maxId = r.id; });
|
||||
const newRow = { id: maxId + 1 };
|
||||
fields.forEach((f) => {
|
||||
newRow[f.name] = f.type === 'booleen' ? 0 : null;
|
||||
});
|
||||
rows.push(newRow);
|
||||
return newRow.id;
|
||||
@@ -158,9 +158,14 @@ function forgeApplyAddRowActionOffline(gameData, action) {
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = {
|
||||
forgeApplyVariableActionOffline, forgeApplyScoreActionOffline, forgeApplyStatusActionOffline,
|
||||
forgeApplyDataActionOffline, forgeApplyAddRowActionOffline,
|
||||
forgeFindFieldMeta, forgeClampToFieldBounds, FORGE_SCORE_STATUS_KEYS,
|
||||
forgeApplyVariableActionOffline,
|
||||
forgeApplyScoreActionOffline,
|
||||
forgeApplyStatusActionOffline,
|
||||
forgeApplyDataActionOffline,
|
||||
forgeApplyAddRowActionOffline,
|
||||
forgeFindFieldMeta,
|
||||
forgeClampToFieldBounds,
|
||||
FORGE_SCORE_STATUS_KEYS,
|
||||
};
|
||||
// node:test charge ce fichier isolément (voir __tests__/) — forgeComputeNewValue
|
||||
// vit dans compute-operation.js, jamais un <script> séparé en test.
|
||||
|
||||
Reference in New Issue
Block a user