seo-generator-server/lib/ValidationGuards.js
StillHammer cd79ca9a4a chore: Add documentation, scripts and monitoring tools
- Add comprehensive documentation (IMPLEMENTATION_COMPLETE, ProductionReady, QUICK_START, STARTUP_ANALYSIS)
- Add startup scripts (start-server.sh, start-server.bat, check-setup.sh)
- Add configs directory structure with README
- Add ValidationGuards and Main.js backup
- Add LLM monitoring HTML interface
- Add cache templates and XML files
- Add technical report (rapport_technique.md)
- Add bundled code.js

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-12 16:10:56 +08:00

283 lines
8.4 KiB
JavaScript

// ========================================
// 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
};