feat(v2): add auditable final score combining actionability, baseline and cooldown
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
014c48d578
commit
749be7dbae
@@ -0,0 +1,54 @@
|
||||
const { computeCooldown } = require('./cooldown');
|
||||
|
||||
const CLASSIFICATION_BASE_POINTS = {
|
||||
ACTIONABLE_NOW: 200,
|
||||
UNASSIGNED_ACTIONABLE: 180,
|
||||
WAITING_APPROVAL: 40,
|
||||
INSUFFICIENT_CONTEXT: 0,
|
||||
WAITING_REQUESTER: -150,
|
||||
WAITING_VENDOR: -150,
|
||||
DEPENDENCY_BLOCKED: -150,
|
||||
PROJECT_LONG_TERM: -120,
|
||||
DUPLICATE_OR_OBSOLETE: -200,
|
||||
SCHEDULED: -1000,
|
||||
NEEDS_COORDINATION: -1000,
|
||||
};
|
||||
|
||||
function computeScore({ ticket, aiClassification, memory, contextChanged, config, now }) {
|
||||
const classification = aiClassification.classification;
|
||||
const cooldown = computeCooldown({ ticket, classification, contextChanged, memory, now, config });
|
||||
|
||||
const baseline = Math.min(Number(ticket.score_criticidade) || 0, 150) * 0.4;
|
||||
const semTecnicoBonus = ticket.sem_tecnico ? 30 : 0;
|
||||
|
||||
let actionabilityPoints = 0;
|
||||
if (!aiClassification.is_fallback) {
|
||||
actionabilityPoints = CLASSIFICATION_BASE_POINTS[classification] ?? 0;
|
||||
if (actionabilityPoints > 0 && Number(aiClassification.confidence) < config.aiConfidenceThreshold) {
|
||||
actionabilityPoints /= 2;
|
||||
}
|
||||
}
|
||||
|
||||
const scheduledNotDue = classification === 'SCHEDULED';
|
||||
const excludedFromTop10 = cooldown.blocked || classification === 'NEEDS_COORDINATION' || scheduledNotDue;
|
||||
|
||||
const total = baseline + semTecnicoBonus + actionabilityPoints - cooldown.penalty;
|
||||
|
||||
let group = 'ranking_geral';
|
||||
const actionableIsh = classification === 'ACTIONABLE_NOW' || classification === 'UNASSIGNED_ACTIONABLE' || aiClassification.is_fallback;
|
||||
if (actionableIsh && ticket.sla_vencido && (ticket.prioridade >= 4 || ticket.urgencia >= 4)) group = 'critico_acionavel';
|
||||
else if (actionableIsh && ticket.sem_tecnico) group = 'sem_tecnico';
|
||||
else if (actionableIsh && ticket.sla_vencido) group = 'sla_vencido_acionavel';
|
||||
else if (actionableIsh && ticket.dias_sem_interacao <= 2) group = 'quick_win';
|
||||
|
||||
return {
|
||||
score: Math.round(total * 100) / 100,
|
||||
group,
|
||||
excludedFromTop10,
|
||||
exclusionReason: excludedFromTop10 ? (scheduledNotDue ? 'scheduled_not_due' : cooldown.reason) : null,
|
||||
breakdown: { baseline, semTecnicoBonus, actionabilityPoints, cooldownPenalty: cooldown.penalty },
|
||||
cooldown,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { computeScore, CLASSIFICATION_BASE_POINTS };
|
||||
@@ -0,0 +1,65 @@
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { computeScore } = require('./score');
|
||||
|
||||
const CONFIG = { aiConfidenceThreshold: 0.6, cooldownDays: 1 };
|
||||
const NOW = new Date('2026-08-24T08:00:00Z').getTime();
|
||||
|
||||
function ticket(overrides) {
|
||||
return Object.assign({
|
||||
id: 1, sla_vencido: false, sem_tecnico: false, dias_sem_interacao: 5, dias_aberto: 5,
|
||||
prioridade: 3, urgencia: 3, score_criticidade: 100,
|
||||
}, overrides);
|
||||
}
|
||||
|
||||
function ai(overrides) {
|
||||
return Object.assign({
|
||||
classification: 'ACTIONABLE_NOW', actionable_today: true, confidence: 0.9,
|
||||
is_fallback: false, reason: 'x', evidence: [],
|
||||
}, overrides);
|
||||
}
|
||||
|
||||
test('chamado acionavel + SLA vencido + prioridade alta cai no grupo critico_acionavel com score alto', () => {
|
||||
const t = ticket({ sla_vencido: true, prioridade: 5, urgencia: 5 });
|
||||
const result = computeScore({ ticket: t, aiClassification: ai({}), memory: null, contextChanged: true, config: CONFIG, now: NOW });
|
||||
assert.equal(result.group, 'critico_acionavel');
|
||||
assert.ok(result.score > 200);
|
||||
assert.equal(result.excludedFromTop10, false);
|
||||
});
|
||||
|
||||
test('WAITING_REQUESTER sem mudanca nao monopoliza (score baixo)', () => {
|
||||
const t = ticket({});
|
||||
const acionavel = computeScore({ ticket: t, aiClassification: ai({}), memory: null, contextChanged: true, config: CONFIG, now: NOW });
|
||||
const esperando = computeScore({ ticket: t, aiClassification: ai({ classification: 'WAITING_REQUESTER' }), memory: null, contextChanged: true, config: CONFIG, now: NOW });
|
||||
assert.ok(esperando.score < acionavel.score);
|
||||
});
|
||||
|
||||
test('SCHEDULED nao entra antes da data (excluido do Top10)', () => {
|
||||
const result = computeScore({ ticket: ticket({}), aiClassification: ai({ classification: 'SCHEDULED' }), memory: null, contextChanged: true, config: CONFIG, now: NOW });
|
||||
assert.equal(result.excludedFromTop10, true);
|
||||
assert.equal(result.exclusionReason, 'scheduled_not_due');
|
||||
});
|
||||
|
||||
test('sem tecnico e acionavel recebe bonus e grupo sem_tecnico', () => {
|
||||
const t = ticket({ sem_tecnico: true, sla_vencido: false });
|
||||
const result = computeScore({ ticket: t, aiClassification: ai({ classification: 'UNASSIGNED_ACTIONABLE' }), memory: null, contextChanged: true, config: CONFIG, now: NOW });
|
||||
assert.equal(result.group, 'sem_tecnico');
|
||||
assert.ok(result.breakdown.semTecnicoBonus > 0);
|
||||
});
|
||||
|
||||
test('confianca baixa reduz o peso da acionabilidade', () => {
|
||||
const t = ticket({});
|
||||
const altaConfianca = computeScore({ ticket: t, aiClassification: ai({ confidence: 0.9 }), memory: null, contextChanged: true, config: CONFIG, now: NOW });
|
||||
const baixaConfianca = computeScore({ ticket: t, aiClassification: ai({ confidence: 0.2 }), memory: null, contextChanged: true, config: CONFIG, now: NOW });
|
||||
assert.ok(baixaConfianca.breakdown.actionabilityPoints < altaConfianca.breakdown.actionabilityPoints);
|
||||
});
|
||||
|
||||
test('NEEDS_COORDINATION e excluido do Top10 diario', () => {
|
||||
const result = computeScore({ ticket: ticket({}), aiClassification: ai({ classification: 'NEEDS_COORDINATION' }), memory: null, contextChanged: true, config: CONFIG, now: NOW });
|
||||
assert.equal(result.excludedFromTop10, true);
|
||||
});
|
||||
|
||||
test('fallback nao recebe bonus de acionabilidade (comportamento seguro tipo V1)', () => {
|
||||
const result = computeScore({ ticket: ticket({}), aiClassification: ai({ is_fallback: true }), memory: null, contextChanged: true, config: CONFIG, now: NOW });
|
||||
assert.equal(result.breakdown.actionabilityPoints, 0);
|
||||
});
|
||||
Reference in New Issue
Block a user