diff --git a/logic/diversity.js b/logic/diversity.js new file mode 100644 index 0000000..7d209b5 --- /dev/null +++ b/logic/diversity.js @@ -0,0 +1,34 @@ +function selectTop(scored, topN, config) { + const eligible = scored.filter((item) => !item.excludedFromTop10); + const excluded = scored.filter((item) => item.excludedFromTop10); + + const sorted = [...eligible].sort((a, b) => b.score - a.score); + + 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 }; diff --git a/logic/diversity.test.js b/logic/diversity.test.js new file mode 100644 index 0000000..635f132 --- /dev/null +++ b/logic/diversity.test.js @@ -0,0 +1,37 @@ +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const { selectTop } = require('./diversity'); + +function item(id, score, group, excludedFromTop10) { + return { ticket: { id }, score, group, excludedFromTop10: Boolean(excludedFromTop10), aiClassification: {} }; +} + +test('Top final nunca ultrapassa topN', () => { + const scored = Array.from({ length: 20 }, (_, i) => item(i, 100 - i, 'ranking_geral', false)); + const { selected } = selectTop(scored, 10, {}); + assert.equal(selected.length, 10); +}); + +test('itens excluidos do Top10 nunca aparecem selecionados', () => { + const scored = [item(1, 500, 'critico_acionavel', true), item(2, 10, 'ranking_geral', false)]; + const { selected } = selectTop(scored, 10, {}); + assert.ok(!selected.some((s) => s.ticket.id === 1)); +}); + +test('10 criticos legitimos no mesmo grupo entram todos (diversidade nao bloqueia emergencia real)', () => { + const scored = Array.from({ length: 10 }, (_, i) => item(i, 500 - i, 'critico_acionavel', false)); + const { selected } = selectTop(scored, 10, {}); + assert.equal(selected.length, 10); + assert.deepEqual(selected.map((s) => s.ticket.id).sort(), scored.map((s) => s.ticket.id).sort()); +}); + +test('com grupos variados, ordena por score respeitando diversidade leve', () => { + const scored = [ + ...Array.from({ length: 8 }, (_, i) => item(`c${i}`, 300 - i, 'critico_acionavel', false)), + item('t1', 250, 'sem_tecnico', false), + item('t2', 240, 'quick_win', false), + ]; + const { selected } = selectTop(scored, 10, {}); + assert.equal(selected.length, 10); + assert.ok(selected.some((s) => s.ticket.id === 't1')); +});