applyFallbackPolicy (new, in logic/cooldown.js alongside the isCriticalOverride it reuses) is now the single place that decides whether an AI_FALLBACK or INSUFFICIENT_CONTEXT ticket can enter the Top10: blocked by default, let through only via an explicit critical override (SLA vencido + prioridade/ urgencia alta, OR sem_tecnico + prioridade/urgencia alta -- isCriticalOverride extended to cover both, previously only covered SLA+priority). score.js calls this policy and sets excludedFromTop10/exclusionReason/decisionReason accordingly, with an auditable *_critical_override or *_no_override reason. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
71 lines
3.0 KiB
JavaScript
71 lines
3.0 KiB
JavaScript
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 };
|