// ======================================== // FICHIER: ValidationGuards.js // SOLUTION D: Validations guard pour détecter problèmes de synchronisation // ======================================== const { logSh } = require('./ErrorReporting'); /** * PATTERNS DE PLACEHOLDERS DÉTECTÉS */ const PLACEHOLDER_PATTERNS = { // Pattern "[XXX non défini]" ou "[XXX non résolu]" unresolvedVariable: /\[([^\]]+) non (défini|résolu)\]/g, // Pattern "{{XXX}}" (variable brute non résolue) rawVariable: /\{\{([^}]+)\}\}/g, // Pattern vide ou null empty: /^\s*$/ }; /** * Vérifie si une chaîne contient des placeholders non résolus * @param {string} text - Texte à vérifier * @param {Object} options - Options de vérification * @returns {Object} Résultat de la validation */ function hasUnresolvedPlaceholders(text, options = {}) { const { checkRawVariables = false, // Vérifier aussi les {{XXX}} allowEmpty = false // Autoriser les textes vides } = options; if (!text || typeof text !== 'string') { return { hasIssues: !allowEmpty, issues: !allowEmpty ? ['Text is null or not a string'] : [], placeholders: [] }; } const issues = []; const placeholders = []; // Vérifier "[XXX non défini]" ou "[XXX non résolu]" const unresolvedMatches = text.match(PLACEHOLDER_PATTERNS.unresolvedVariable); if (unresolvedMatches) { placeholders.push(...unresolvedMatches); issues.push(`Found ${unresolvedMatches.length} unresolved placeholders: ${unresolvedMatches.join(', ')}`); } // Vérifier "{{XXX}}" si demandé if (checkRawVariables) { const rawMatches = text.match(PLACEHOLDER_PATTERNS.rawVariable); if (rawMatches) { placeholders.push(...rawMatches); issues.push(`Found ${rawMatches.length} raw variables: ${rawMatches.join(', ')}`); } } // Vérifier vide if (!allowEmpty && PLACEHOLDER_PATTERNS.empty.test(text)) { issues.push('Text is empty or whitespace only'); } return { hasIssues: issues.length > 0, issues, placeholders }; } /** * GUARD: Valider un élément avant utilisation dans génération * Throw une erreur si l'élément contient des placeholders non résolus * @param {Object} element - Élément à valider * @param {Object} options - Options de validation * @throws {Error} Si validation échoue */ function validateElement(element, options = {}) { const { strict = true, // Mode strict: throw error checkInstructions = true, // Vérifier les instructions checkContent = true, // Vérifier le contenu résolu context = '' // Contexte pour message d'erreur } = options; const errors = []; // Vérifier resolvedContent if (checkContent && element.resolvedContent) { const contentCheck = hasUnresolvedPlaceholders(element.resolvedContent, { allowEmpty: false }); if (contentCheck.hasIssues) { errors.push({ field: 'resolvedContent', content: element.resolvedContent.substring(0, 100), issues: contentCheck.issues, placeholders: contentCheck.placeholders }); } } // Vérifier instructions if (checkInstructions && element.instructions) { const instructionsCheck = hasUnresolvedPlaceholders(element.instructions, { allowEmpty: false }); if (instructionsCheck.hasIssues) { errors.push({ field: 'instructions', content: element.instructions.substring(0, 100), issues: instructionsCheck.issues, placeholders: instructionsCheck.placeholders }); } } // Si erreurs trouvées if (errors.length > 0) { const errorMessage = ` ❌ VALIDATION FAILED: Element [${element.name || 'unknown'}] contains unresolved placeholders ${context ? `Context: ${context}` : ''} Errors found: ${errors.map((err, i) => ` ${i + 1}. Field: ${err.field} Content preview: "${err.content}..." Issues: ${err.issues.join('; ')} Placeholders: ${err.placeholders.join(', ')} `).join('\n')} ⚠️ This indicates a synchronization problem between resolvedContent and instructions. 💡 Check that MissingKeywords.js properly updates BOTH fields when generating keywords. `; logSh(errorMessage, 'ERROR'); if (strict) { throw new Error(`Element [${element.name}] validation failed: ${errors.map(e => e.issues.join('; ')).join(' | ')}`); } return { valid: false, errors }; } logSh(`✅ Validation OK: Element [${element.name}] has no unresolved placeholders`, 'DEBUG'); return { valid: true, errors: [] }; } /** * GUARD: Valider une hiérarchie complète * @param {Object} hierarchy - Hiérarchie à valider * @param {Object} options - Options de validation * @returns {Object} Résultat de validation avec statistiques */ function validateHierarchy(hierarchy, options = {}) { const { strict = false // En mode non-strict, on continue même avec des erreurs } = options; const stats = { totalElements: 0, validElements: 0, invalidElements: 0, errors: [] }; Object.entries(hierarchy).forEach(([path, section]) => { // Valider titre if (section.title && section.title.originalElement) { stats.totalElements++; try { const result = validateElement(section.title.originalElement, { strict, context: `Hierarchy path: ${path} (title)` }); if (result.valid) { stats.validElements++; } else { stats.invalidElements++; stats.errors.push({ path, element: 'title', errors: result.errors }); } } catch (error) { stats.invalidElements++; stats.errors.push({ path, element: 'title', error: error.message }); if (strict) throw error; } } // Valider texte if (section.text && section.text.originalElement) { stats.totalElements++; try { const result = validateElement(section.text.originalElement, { strict, context: `Hierarchy path: ${path} (text)` }); if (result.valid) { stats.validElements++; } else { stats.invalidElements++; stats.errors.push({ path, element: 'text', errors: result.errors }); } } catch (error) { stats.invalidElements++; stats.errors.push({ path, element: 'text', error: error.message }); if (strict) throw error; } } // Valider questions FAQ if (section.questions && section.questions.length > 0) { section.questions.forEach((q, index) => { if (q.originalElement) { stats.totalElements++; try { const result = validateElement(q.originalElement, { strict, context: `Hierarchy path: ${path} (question ${index + 1})` }); if (result.valid) { stats.validElements++; } else { stats.invalidElements++; stats.errors.push({ path, element: `question_${index + 1}`, errors: result.errors }); } } catch (error) { stats.invalidElements++; stats.errors.push({ path, element: `question_${index + 1}`, error: error.message }); if (strict) throw error; } } }); } }); // Log résumé if (stats.invalidElements > 0) { logSh(`⚠️ Hierarchy validation: ${stats.invalidElements}/${stats.totalElements} elements have unresolved placeholders`, 'WARNING'); logSh(` Errors: ${JSON.stringify(stats.errors, null, 2)}`, 'WARNING'); } else { logSh(`✅ Hierarchy validation: All ${stats.totalElements} elements are valid`, 'INFO'); } return { valid: stats.invalidElements === 0, stats }; } /** * HELPER: Extraire tous les placeholders d'un texte * @param {string} text - Texte à analyser * @returns {Array} Liste des placeholders trouvés */ function extractPlaceholders(text) { if (!text || typeof text !== 'string') return []; const placeholders = []; // Extraire "[XXX non défini]" const unresolvedMatches = text.match(PLACEHOLDER_PATTERNS.unresolvedVariable); if (unresolvedMatches) { placeholders.push(...unresolvedMatches); } // Extraire "{{XXX}}" const rawMatches = text.match(PLACEHOLDER_PATTERNS.rawVariable); if (rawMatches) { placeholders.push(...rawMatches); } return placeholders; } module.exports = { // Fonctions principales validateElement, validateHierarchy, hasUnresolvedPlaceholders, // Helpers extractPlaceholders, PLACEHOLDER_PATTERNS };