feat(v2): add Top10 selection with soft group diversity, hard 10-item cap
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
749be7dbae
commit
d3917707e0
@@ -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 };
|
||||||
@@ -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'));
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user