feat(v2): add data-quality audit queue for INSUFFICIENT_CONTEXT, require non-empty next_action

buildDataQualityReview extracts an auditable record per INSUFFICIENT_CONTEXT
ticket (ticket_id, title, ai_reason, context_size, has_description,
followup_count, task_count, has_solution, last_interaction_at) -- not sent
anywhere, just for calibration. Also tightened validateAiClassification to
reject an empty/whitespace-only next_action (was previously accepted).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Gustavo
2026-08-24 16:52:22 -03:00
co-authored by Claude Sonnet 5
parent 34f61abc3b
commit 485234dc88
4 changed files with 81 additions and 1 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ function validateAiClassification(raw) {
if (typeof raw.actionable_today !== 'boolean') errors.push('actionable_today deve ser boolean');
const confidence = Number(raw.confidence);
if (!Number.isFinite(confidence) || confidence < 0 || confidence > 1) errors.push('confidence deve estar entre 0 e 1');
if (typeof raw.next_action !== 'string' || raw.next_action.length > 200) errors.push('next_action invalido');
if (typeof raw.next_action !== 'string' || raw.next_action.trim().length === 0 || raw.next_action.length > 200) errors.push('next_action invalido (vazio ou muito longo)');
if (raw.evidence != null && !Array.isArray(raw.evidence)) errors.push('evidence deve ser array');
if (typeof raw.reason !== 'string') errors.push('reason deve ser string');
+5
View File
@@ -36,6 +36,11 @@ test('JSON invalido/null nao lanca excecao', () => {
assert.equal(validateAiClassification(null).valid, false);
});
test('next_action vazia ou so espacos falha (nao vazia e requisito minimo de qualidade)', () => {
assert.equal(validateAiClassification(validRaw({ next_action: '' })).valid, false);
assert.equal(validateAiClassification(validRaw({ next_action: ' ' })).valid, false);
});
test('confidence fora de 0..1 falha', () => {
const result = validateAiClassification(validRaw({ confidence: 1.5 }));
assert.equal(result.valid, false);
+29
View File
@@ -0,0 +1,29 @@
// Fila interna de auditoria/calibracao para chamados classificados como INSUFFICIENT_CONTEXT.
// Nao e enviada a ninguem -- serve so pra identificar problemas de preenchimento no GLPI
// (ver secao 10/14 da missao original). Fica gravada dentro do resumo do shadow run.
function buildDataQualityReview(items) {
return items
.filter((item) => item.aiClassification.classification === 'INSUFFICIENT_CONTEXT')
.map((item) => {
const t = item.ticket;
const followups = t.followups || [];
const tasks = t.tasks || [];
const contextSize = (t.descricao || '').length
+ followups.reduce((sum, f) => sum + (f.texto || '').length, 0)
+ tasks.reduce((sum, task) => sum + (task.texto || '').length, 0);
return {
ticket_id: t.id,
title: t.titulo,
ai_reason: item.aiClassification.reason,
context_size: contextSize,
has_description: Boolean(t.descricao && t.descricao.trim().length > 0),
followup_count: followups.length,
task_count: tasks.length,
has_solution: Boolean(t.solucao),
last_interaction_at: t.ultima_interacao_em,
};
});
}
module.exports = { buildDataQualityReview };
+46
View File
@@ -0,0 +1,46 @@
const { test } = require('node:test');
const assert = require('node:assert/strict');
const { buildDataQualityReview } = require('./dataQuality');
function scoredItem(overrides) {
return Object.assign({
ticket: {
id: 33492, titulo: 'Instalar novo servidor', descricao: 'Descricao do chamado',
followups: [{ id: 1, texto: 'ok' }], tasks: [], ultima_interacao_em: '2025-11-06 15:49:15',
},
aiClassification: { classification: 'INSUFFICIENT_CONTEXT', reason: 'Sem evidencia de proxima acao.' },
}, overrides);
}
test('inclui apenas chamados INSUFFICIENT_CONTEXT', () => {
const items = [
scoredItem({}),
scoredItem({ aiClassification: { classification: 'ACTIONABLE_NOW', reason: 'x' } }),
];
const review = buildDataQualityReview(items);
assert.equal(review.length, 1);
assert.equal(review[0].ticket_id, 33492);
});
test('registro traz os campos minimos pedidos para auditoria', () => {
const review = buildDataQualityReview([scoredItem({})]);
const entry = review[0];
assert.equal(entry.ticket_id, 33492);
assert.equal(entry.title, 'Instalar novo servidor');
assert.equal(entry.ai_reason, 'Sem evidencia de proxima acao.');
assert.equal(entry.has_description, true);
assert.equal(entry.followup_count, 1);
assert.equal(entry.task_count, 0);
assert.equal(entry.last_interaction_at, '2025-11-06 15:49:15');
assert.ok(typeof entry.context_size === 'number' && entry.context_size > 0);
});
test('has_description e false quando nao ha descricao real', () => {
const review = buildDataQualityReview([scoredItem({ ticket: { ...scoredItem({}).ticket, descricao: '' } })]);
assert.equal(review[0].has_description, false);
});
test('lista vazia quando nao ha nenhum INSUFFICIENT_CONTEXT', () => {
const items = [scoredItem({ aiClassification: { classification: 'ACTIONABLE_NOW', reason: 'x' } })];
assert.deepEqual(buildDataQualityReview(items), []);
});