Files
SLACK_COORD_RELATORIO/coordenador/logic/cooldown.js
T
Gustavo Santos e322cec92b chore: import coordenador (subtree)
git-subtree-dir: coordenador
git-subtree-mainline: facbe2e340
git-subtree-split: ce7af94d55
2026-10-01 14:25:55 -03:00

90 lines
4.2 KiB
JavaScript

const ONE_DAY_MS = 24 * 60 * 60 * 1000;
// Classes que NUNCA disputam o Top10 operacional -- bloqueadas por definicao, nao por cache.
// Lista expandida na missao "finalizacao para producao" (secao 10): WAITING_APPROVAL,
// PROJECT_LONG_TERM e DUPLICATE_OR_OBSOLETE entraram aqui (antes so tinham penalidade de
// score, o que deixava WAITING_APPROVAL -- pontuacao base positiva -- competir de verdade).
const BLOCKING_CLASSIFICATIONS = new Set([
'WAITING_REQUESTER', 'WAITING_VENDOR', 'WAITING_APPROVAL', 'SCHEDULED',
'DEPENDENCY_BLOCKED', 'PROJECT_LONG_TERM', 'DUPLICATE_OR_OBSOLETE',
]);
// Regra centralizada de "isso e critico o bastante pra furar uma exclusao normal" --
// usada tanto pelo cooldown de reenvio quanto pela politica de AI_FALLBACK/
// INSUFFICIENT_CONTEXT (applyFallbackPolicy abaixo). Unico lugar que define "critico".
function isCriticalOverride(ticket) {
const prioridadeAlta = Number(ticket.prioridade) >= 4 || Number(ticket.urgencia) >= 4;
const slaVencidoCritico = Boolean(ticket.sla_vencido) && prioridadeAlta;
const semTecnicoCritico = Boolean(ticket.sem_tecnico) && prioridadeAlta;
return slaVencidoCritico || semTecnicoCritico;
}
// AI_FALLBACK (chamado nunca analisado pela IA de verdade) e INSUFFICIENT_CONTEXT
// (analisado, mas sem evidencia suficiente) NAO disputam o Top10 normalmente -- so entram
// via excecao deterministica forte (isCriticalOverride). Centraliza a regra pedida:
// "AI_FALLBACK nao pode competir como se fosse acionavel confirmado".
function applyFallbackPolicy({ aiClassification, ticket }) {
const critical = isCriticalOverride(ticket);
if (aiClassification.is_fallback) {
return critical
? { blocked: false, decisionReason: 'ai_fallback_critical_override' }
: { blocked: true, decisionReason: 'ai_fallback_no_override' };
}
if (aiClassification.classification === 'INSUFFICIENT_CONTEXT') {
return critical
? { blocked: false, decisionReason: 'insufficient_context_critical_override' }
: { blocked: true, decisionReason: 'insufficient_context_no_override' };
}
return { blocked: false, decisionReason: null };
}
function computeCooldown({ ticket, classification, contextChanged, memory, now, config }) {
const consecutiveDaysSent = memory ? Number(memory.consecutive_days_sent || 0) : 0;
const lastSentAt = memory ? memory.last_sent_at : null;
const nextReviewAt = memory ? memory.next_review_at : null;
const critical = isCriticalOverride(ticket);
let blocked = false;
let penalty = 0;
let reason = 'ok';
let breakCooldown = false;
if (classification === 'NEEDS_COORDINATION') {
blocked = true;
reason = 'needs_coordination';
} else if (nextReviewAt != null && now < nextReviewAt && !contextChanged) {
blocked = true;
reason = 'waiting_next_review';
} else if (BLOCKING_CLASSIFICATIONS.has(classification)) {
// BUG REAL comprovado na Run A anterior: WAITING_VENDOR entrou no Top10 so porque o
// contexto tinha mudado nesta execucao (contextChanged=true). contextChanged decide SE
// reanalisamos (logic/cache.js) -- nunca decide se um chamado que CONTINUA classificado
// numa classe bloqueada pode competir. Por isso este bloqueio e incondicional aqui: se a
// classificacao atual (fresca ou cacheada, tanto faz) esta em BLOCKING_CLASSIFICATIONS,
// fica fora do Top10, ponto. Missao "finalizacao para producao", secao 10.
blocked = true;
reason = `blocked_${classification.toLowerCase()}`;
}
const sentRecently = lastSentAt != null && (now - lastSentAt) < ONE_DAY_MS * 1.5;
if (!contextChanged && sentRecently && consecutiveDaysSent > 0) {
const perDayBase = classification === 'ACTIONABLE_NOW' || classification === 'UNASSIGNED_ACTIONABLE' ? 10 : 15;
penalty = Math.min(90, perDayBase * consecutiveDaysSent);
if (!blocked) reason = 'repeat_no_change';
}
if (blocked && critical && classification !== 'NEEDS_COORDINATION') {
breakCooldown = true;
blocked = false;
penalty = Math.round(penalty / 2);
reason = `${reason}_override_critical`;
}
return { blocked, penalty, reason, breakCooldown, consecutiveDaysSent };
}
module.exports = { computeCooldown, isCriticalOverride, applyFallbackPolicy, BLOCKING_CLASSIFICATIONS };