function selectTop(scored, topN, config) { const eligible = scored.filter((item) => !item.excludedFromTop10); const excluded = scored.filter((item) => item.excludedFromTop10); // BUG REAL comprovado em execucao ao vivo (missao "correcao final", secao 14): Array.sort e // estavel, e muitos chamados empatam em score -- sem um criterio de desempate deterministico, // a ordem de chegada de $input.all() (que o n8n NAO garante entre branches paralelos -- // cache/ia/fallback se juntam de novo antes daqui) decidia o desempate. Duas execucoes // seguidas com os MESMOS dados produziram Top10 DIFERENTES so por causa disso, gerando duas // mensagens reais distintas pro Slack no mesmo dia. ticket.id como criterio secundario // garante o mesmo resultado sempre que os scores forem os mesmos -- nao muda QUEM vence // (isso continua sendo so o score), so desempata de forma reproduzivel. const sorted = [...eligible].sort((a, b) => { if (b.score !== a.score) return b.score - a.score; return String(a.ticket.id).localeCompare(String(b.ticket.id)); }); const groupCap = Math.max(1, Math.ceil(topN * 0.5)); const groupCounts = {}; const firstPass = []; const deferred = []; for (const item of sorted) { const count = groupCounts[item.group] || 0; if (count < groupCap && firstPass.length < topN) { firstPass.push(item); groupCounts[item.group] = count + 1; } else { deferred.push(item); } } let selected = firstPass; if (selected.length < topN) { selected = selected.concat(deferred.slice(0, topN - selected.length)); } selected = selected.slice(0, topN); const selectedIds = new Set(selected.map((item) => item.ticket.id)); const notSelected = sorted.filter((item) => !selectedIds.has(item.ticket.id)); return { selected, excluded: excluded.concat(notSelected) }; } module.exports = { selectTop };