fix(v2): centralize AI_FALLBACK/INSUFFICIENT_CONTEXT eligibility policy in cooldown.js

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>
This commit is contained in:
Gustavo
2026-08-24 16:51:17 -03:00
co-authored by Claude Sonnet 5
parent 00a42db269
commit 34f61abc3b
4 changed files with 118 additions and 7 deletions
+19 -3
View File
@@ -1,4 +1,4 @@
const { computeCooldown } = require('./cooldown');
const { computeCooldown, applyFallbackPolicy } = require('./cooldown');
const CLASSIFICATION_BASE_POINTS = {
ACTIONABLE_NOW: 200,
@@ -12,11 +12,16 @@ const CLASSIFICATION_BASE_POINTS = {
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;
@@ -30,7 +35,7 @@ function computeScore({ ticket, aiClassification, memory, contextChanged, config
}
const scheduledNotDue = classification === 'SCHEDULED';
const excludedFromTop10 = cooldown.blocked || classification === 'NEEDS_COORDINATION' || scheduledNotDue;
const excludedFromTop10 = cooldown.blocked || classification === 'NEEDS_COORDINATION' || scheduledNotDue || fallbackPolicy.blocked;
const total = baseline + semTecnicoBonus + actionabilityPoints - cooldown.penalty;
@@ -41,11 +46,22 @@ function computeScore({ ticket, aiClassification, memory, contextChanged, config
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: excludedFromTop10 ? (scheduledNotDue ? 'scheduled_not_due' : cooldown.reason) : null,
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,
};