// ======================================== // GLOBAL BUDGET MANAGER - Gestion budget global expressions familières // Responsabilité: Empêcher spam en distribuant budget limité sur tous les tags // Architecture: Budget défini par personality, distribué aléatoirement, consommé et tracké // ======================================== const { logSh } = require('../ErrorReporting'); /** * GLOBAL BUDGET MANAGER * Gère un budget global d'expressions familières pour toute une génération */ class GlobalBudgetManager { constructor(personality, contentMap = {}, config = {}) { this.personality = personality; this.contentMap = contentMap; this.config = config; // Paramètre configurable : caractères par expression (default 4000) this.charsPerExpression = config.charsPerExpression || 4000; // Calculer taille totale du contenu const totalChars = Object.values(contentMap).join('').length; this.totalChars = totalChars; // Budget par défaut (sera écrasé par calcul dynamique) this.defaultBudget = { costaud: 2, nickel: 2, tipTop: 1, impeccable: 2, solide: 3, total: 10 }; // === ✅ NOUVEAU: Calculer budget dynamiquement === this.budget = this.calculateDynamicBudget(contentMap, personality); // Tracking consommation this.consumed = {}; this.totalConsumed = 0; // Assignments par tag this.tagAssignments = {}; } /** * ✅ NOUVEAU: Calculer budget dynamiquement selon taille texte * Formule : budgetTotal = Math.floor(totalChars / charsPerExpression) * Puis soustraire occurrences existantes */ calculateDynamicBudget(contentMap, personality) { const fullText = Object.values(contentMap).join(' '); const totalChars = fullText.length; // Calculer budget total basé sur taille const budgetTotal = Math.max(1, Math.floor(totalChars / this.charsPerExpression)); logSh(`📏 Taille texte: ${totalChars} chars → Budget calculé: ${budgetTotal} expressions (${this.charsPerExpression} chars/expr)`, 'INFO'); // Compter occurrences existantes de chaque expression const fullTextLower = fullText.toLowerCase(); const existingOccurrences = { costaud: (fullTextLower.match(/costaud/g) || []).length, nickel: (fullTextLower.match(/nickel/g) || []).length, tipTop: (fullTextLower.match(/tip[\s-]?top/g) || []).length, impeccable: (fullTextLower.match(/impeccable/g) || []).length, solide: (fullTextLower.match(/solide/g) || []).length }; const totalExisting = Object.values(existingOccurrences).reduce((sum, count) => sum + count, 0); logSh(`📊 Occurrences existantes: ${JSON.stringify(existingOccurrences)} (total: ${totalExisting})`, 'DEBUG'); // Budget disponible = Budget calculé - Occurrences existantes let budgetAvailable = Math.max(0, budgetTotal - totalExisting); logSh(`💰 Budget disponible après soustraction: ${budgetAvailable}/${budgetTotal}`, 'INFO'); // Si aucun budget disponible, retourner budget vide if (budgetAvailable === 0) { logSh(`⚠️ Budget épuisé par occurrences existantes, aucune expression familière supplémentaire autorisée`, 'WARN'); return { costaud: 0, nickel: 0, tipTop: 0, impeccable: 0, solide: 0, total: 0 }; } // Distribuer budget disponible selon style personality return this.distributeBudgetByStyle(budgetAvailable, personality); } /** * Distribuer budget selon style personality */ distributeBudgetByStyle(budgetTotal, personality) { if (!personality || !personality.style) { // Fallback: distribution équitable return this.equalDistribution(budgetTotal); } const style = personality.style.toLowerCase(); if (style.includes('familier') || style.includes('accessible')) { // Kévin-like: plus d'expressions familières variées return { costaud: Math.floor(budgetTotal * 0.25), nickel: Math.floor(budgetTotal * 0.25), tipTop: Math.floor(budgetTotal * 0.15), impeccable: Math.floor(budgetTotal * 0.2), solide: Math.floor(budgetTotal * 0.15), total: budgetTotal }; } else if (style.includes('technique') || style.includes('précis')) { // Marc-like: presque pas d'expressions familières (tout sur "solide") return { costaud: 0, nickel: 0, tipTop: 0, impeccable: Math.floor(budgetTotal * 0.3), solide: Math.floor(budgetTotal * 0.7), total: budgetTotal }; } // Fallback: distribution équitable return this.equalDistribution(budgetTotal); } /** * Distribution équitable du budget */ equalDistribution(budgetTotal) { const perExpression = Math.floor(budgetTotal / 5); return { costaud: perExpression, nickel: perExpression, tipTop: perExpression, impeccable: perExpression, solide: perExpression, total: budgetTotal }; } /** * Initialiser budget depuis personality ou fallback default (LEGACY - non utilisé) */ initializeBudget(personality) { if (personality?.budgetExpressions) { logSh(`📊 Budget expressions depuis personality: ${JSON.stringify(personality.budgetExpressions)}`, 'DEBUG'); return { ...personality.budgetExpressions }; } // Fallback: adapter selon style personality if (personality?.style) { const style = personality.style.toLowerCase(); if (style.includes('familier') || style.includes('accessible')) { // Kévin-like: plus d'expressions familières autorisées return { costaud: 2, nickel: 2, tipTop: 1, impeccable: 2, solide: 3, total: 10 }; } else if (style.includes('technique') || style.includes('précis')) { // Marc-like: presque pas d'expressions familières return { costaud: 0, nickel: 0, tipTop: 0, impeccable: 1, solide: 2, total: 3 }; } } // Fallback ultime logSh(`📊 Budget expressions par défaut: ${JSON.stringify(this.defaultBudget)}`, 'DEBUG'); return { ...this.defaultBudget }; } /** * Distribuer budget aléatoirement sur tous les tags * @param {array} tags - Liste des tags à traiter */ distributeRandomly(tags) { logSh(`🎲 Distribution aléatoire du budget sur ${tags.length} tags`, 'DEBUG'); const assignments = {}; // Initialiser tous les tags à 0 tags.forEach(tag => { assignments[tag] = { costaud: 0, nickel: 0, tipTop: 0, impeccable: 0, solide: 0 }; }); // Distribuer chaque expression const expressions = ['costaud', 'nickel', 'tipTop', 'impeccable', 'solide']; expressions.forEach(expr => { const maxCount = this.budget[expr] || 0; for (let i = 0; i < maxCount; i++) { // Choisir tag aléatoire const randomTag = tags[Math.floor(Math.random() * tags.length)]; assignments[randomTag][expr]++; } }); this.tagAssignments = assignments; logSh(`✅ Budget distribué: ${JSON.stringify(assignments)}`, 'DEBUG'); return assignments; } /** * Obtenir budget alloué pour un tag spécifique */ getBudgetForTag(tag) { return this.tagAssignments[tag] || { costaud: 0, nickel: 0, tipTop: 0, impeccable: 0, solide: 0 }; } /** * Consommer budget (marquer comme utilisé) */ consumeBudget(tag, expression, count = 1) { if (!this.consumed[tag]) { this.consumed[tag] = { costaud: 0, nickel: 0, tipTop: 0, impeccable: 0, solide: 0 }; } this.consumed[tag][expression] += count; this.totalConsumed += count; logSh(`📉 Budget consommé [${tag}] ${expression}: ${count} (total: ${this.totalConsumed}/${this.budget.total})`, 'DEBUG'); } /** * Vérifier si on peut encore utiliser une expression */ canUse(tag, expression) { const allocated = this.tagAssignments[tag]?.[expression] || 0; const used = this.consumed[tag]?.[expression] || 0; const canUse = used < allocated && this.totalConsumed < this.budget.total; if (!canUse) { logSh(`🚫 Budget épuisé pour [${tag}] ${expression} (used: ${used}/${allocated}, total: ${this.totalConsumed}/${this.budget.total})`, 'DEBUG'); } return canUse; } /** * Obtenir budget restant global */ getRemainingBudget() { return { remaining: this.budget.total - this.totalConsumed, total: this.budget.total, consumed: this.totalConsumed, percentage: ((this.totalConsumed / this.budget.total) * 100).toFixed(1) }; } /** * Générer rapport final */ getReport() { const remaining = this.getRemainingBudget(); return { budget: this.budget, totalConsumed: this.totalConsumed, remaining: remaining.remaining, percentageUsed: remaining.percentage, consumedByTag: this.consumed, allocatedByTag: this.tagAssignments, overBudget: this.totalConsumed > this.budget.total }; } } module.exports = { GlobalBudgetManager };