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>
79 lines
3.2 KiB
JavaScript
79 lines
3.2 KiB
JavaScript
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
|
|
|
const BLOCKING_CLASSIFICATIONS = new Set([
|
|
'WAITING_REQUESTER', 'WAITING_VENDOR', 'SCHEDULED', 'DEPENDENCY_BLOCKED',
|
|
]);
|
|
|
|
// 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) && !contextChanged) {
|
|
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 };
|