diff --git a/logic/cooldown.js b/logic/cooldown.js index f944e56..2b6ff34 100644 --- a/logic/cooldown.js +++ b/logic/cooldown.js @@ -1,7 +1,12 @@ 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', 'SCHEDULED', 'DEPENDENCY_BLOCKED', + '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" -- @@ -53,7 +58,13 @@ function computeCooldown({ ticket, classification, contextChanged, memory, now, } else if (nextReviewAt != null && now < nextReviewAt && !contextChanged) { blocked = true; reason = 'waiting_next_review'; - } else if (BLOCKING_CLASSIFICATIONS.has(classification) && !contextChanged) { + } 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()}`; } diff --git a/logic/cooldown.test.js b/logic/cooldown.test.js index 0f6af0e..9e24755 100644 --- a/logic/cooldown.test.js +++ b/logic/cooldown.test.js @@ -23,12 +23,39 @@ test('enviado ontem sem mudanca recebe penalidade crescente', () => { assert.equal(result.blocked, false); }); -test('mudanca de contexto real desbloqueia mesmo em estado de espera', () => { +test('BUG REAL corrigido: WAITING_VENDOR continua bloqueado do Top10 mesmo com contextChanged=true -- missao "finalizacao para producao" secao 10', () => { + // Antes deste ciclo, contextChanged=true desbloqueava um chamado que CONTINUAVA + // classificado como WAITING_VENDOR apos a reanalise -- foi exatamente o que aconteceu na + // Run A anterior. contextChanged so deve decidir SE reanalisamos (logic/cache.js), nunca + // SE um chamado ainda bloqueado pode competir no Top10. const memory = { consecutive_days_sent: 2, last_sent_at: NOW - ONE_DAY, next_review_at: NOW + ONE_DAY }; const result = computeCooldown({ ticket: ticket({}), classification: 'WAITING_VENDOR', contextChanged: true, memory, now: NOW, config: CONFIG }); + assert.equal(result.blocked, true); + assert.equal(result.reason, 'blocked_waiting_vendor'); +}); + +test('chamado sai de WAITING_VENDOR para ACTIONABLE_NOW apos evento real (cronologia) -- deixa de ser bloqueado', () => { + // A reclassificacao em si (peca chegou, virou ACTIONABLE_NOW) e responsabilidade do prompt/ + // IA; aqui validamos que, uma vez que a classificacao ja NAO esta mais em + // BLOCKING_CLASSIFICATIONS, o cooldown para de bloquear -- sem depender de contextChanged. + const memory = { consecutive_days_sent: 2, last_sent_at: NOW - ONE_DAY, next_review_at: NOW + ONE_DAY }; + const result = computeCooldown({ ticket: ticket({}), classification: 'ACTIONABLE_NOW', contextChanged: true, memory, now: NOW, config: CONFIG }); assert.equal(result.blocked, false); }); +test('WAITING_APPROVAL tambem fica fora do Top10 incondicionalmente -- classe adicionada nesta missao', () => { + const result = computeCooldown({ ticket: ticket({}), classification: 'WAITING_APPROVAL', contextChanged: true, memory: null, now: NOW, config: CONFIG }); + assert.equal(result.blocked, true); + assert.equal(result.reason, 'blocked_waiting_approval'); +}); + +test('PROJECT_LONG_TERM e DUPLICATE_OR_OBSOLETE tambem ficam fora do Top10 incondicionalmente', () => { + const r1 = computeCooldown({ ticket: ticket({}), classification: 'PROJECT_LONG_TERM', contextChanged: true, memory: null, now: NOW, config: CONFIG }); + const r2 = computeCooldown({ ticket: ticket({}), classification: 'DUPLICATE_OR_OBSOLETE', contextChanged: true, memory: null, now: NOW, config: CONFIG }); + assert.equal(r1.blocked, true); + assert.equal(r2.blocked, true); +}); + test('NEEDS_COORDINATION sempre bloqueado para o Top 10 diario', () => { const result = computeCooldown({ ticket: ticket({}), classification: 'NEEDS_COORDINATION', contextChanged: false, memory: null, now: NOW, config: CONFIG }); assert.equal(result.blocked, true); diff --git a/logic/score.test.js b/logic/score.test.js index 551b3e4..4b58e19 100644 --- a/logic/score.test.js +++ b/logic/score.test.js @@ -89,3 +89,14 @@ test('INSUFFICIENT_CONTEXT critico entra somente pela excecao definida', () => { assert.equal(result.excludedFromTop10, false); assert.equal(result.decisionReason, 'insufficient_context_critical_override'); }); + +test('BUG REAL comprovado na Run A anterior: WAITING_VENDOR fica fora do Top10 mesmo com contextChanged=true -- missao "finalizacao para producao" secao 10', () => { + const result = computeScore({ ticket: ticket({}), aiClassification: ai({ classification: 'WAITING_VENDOR', actionable_today: false, confidence: 0.8 }), memory: null, contextChanged: true, config: CONFIG, now: NOW }); + assert.equal(result.excludedFromTop10, true); + assert.equal(result.exclusionReason, 'blocked_waiting_vendor'); +}); + +test('WAITING_APPROVAL (pontuacao base positiva) tambem fica fora do Top10 -- antes so tinha penalidade de score, nao exclusao', () => { + const result = computeScore({ ticket: ticket({}), aiClassification: ai({ classification: 'WAITING_APPROVAL', actionable_today: false, confidence: 0.8 }), memory: null, contextChanged: true, config: CONFIG, now: NOW }); + assert.equal(result.excludedFromTop10, true); +});