const { computeCooldown, applyFallbackPolicy } = require('./cooldown'); const CLASSIFICATION_BASE_POINTS = { ACTIONABLE_NOW: 200, UNASSIGNED_ACTIONABLE: 180, WAITING_APPROVAL: 40, INSUFFICIENT_CONTEXT: 0, WAITING_REQUESTER: -150, WAITING_VENDOR: -150, DEPENDENCY_BLOCKED: -150, PROJECT_LONG_TERM: -120, DUPLICATE_OR_OBSOLETE: -200, SCHEDULED: -1000, NEEDS_COORDINATION: -1000, AI_FALLBACK: 0, }; function computeScore({ ticket, aiClassification, memory, contextChanged, config, now }) { const classification = aiClassification.classification; const cooldown = computeCooldown({ ticket, classification, contextChanged, memory, now, config }); // AI_FALLBACK (nunca analisado) e INSUFFICIENT_CONTEXT (analisado, sem evidencia) nao // disputam o Top10 normalmente -- so entram por excecao deterministica forte, decidida em // um so lugar (logic/cooldown.js -> applyFallbackPolicy). const fallbackPolicy = applyFallbackPolicy({ aiClassification, ticket }); const baseline = Math.min(Number(ticket.score_criticidade) || 0, 150) * 0.4; const semTecnicoBonus = ticket.sem_tecnico ? 30 : 0; let actionabilityPoints = 0; if (!aiClassification.is_fallback) { actionabilityPoints = CLASSIFICATION_BASE_POINTS[classification] ?? 0; if (actionabilityPoints > 0 && Number(aiClassification.confidence) < config.aiConfidenceThreshold) { actionabilityPoints /= 2; } } const scheduledNotDue = classification === 'SCHEDULED'; const excludedFromTop10 = cooldown.blocked || classification === 'NEEDS_COORDINATION' || scheduledNotDue || fallbackPolicy.blocked; const total = baseline + semTecnicoBonus + actionabilityPoints - cooldown.penalty; let group = 'ranking_geral'; const actionableIsh = classification === 'ACTIONABLE_NOW' || classification === 'UNASSIGNED_ACTIONABLE' || aiClassification.is_fallback; if (actionableIsh && ticket.sla_vencido && (ticket.prioridade >= 4 || ticket.urgencia >= 4)) group = 'critico_acionavel'; else if (actionableIsh && ticket.sem_tecnico) group = 'sem_tecnico'; else if (actionableIsh && ticket.sla_vencido) group = 'sla_vencido_acionavel'; else if (actionableIsh && ticket.dias_sem_interacao <= 2) group = 'quick_win'; let exclusionReason = null; if (excludedFromTop10) { if (scheduledNotDue) exclusionReason = 'scheduled_not_due'; else if (classification === 'NEEDS_COORDINATION') exclusionReason = 'needs_coordination'; else if (fallbackPolicy.blocked) exclusionReason = fallbackPolicy.decisionReason; else exclusionReason = cooldown.reason; } return { score: Math.round(total * 100) / 100, group, excludedFromTop10, exclusionReason, // decisionReason fica preenchido mesmo quando NAO excluido (ex.: *_critical_override), // pra ficar auditavel por que um AI_FALLBACK/INSUFFICIENT_CONTEXT furou a regra. decisionReason: fallbackPolicy.decisionReason, breakdown: { baseline, semTecnicoBonus, actionabilityPoints, cooldownPenalty: cooldown.penalty }, cooldown, }; } module.exports = { computeScore, CLASSIFICATION_BASE_POINTS };