From 485234dc8889926ab06e65a4c3f25668614e3aeb Mon Sep 17 00:00:00 2001 From: Gustavo Date: Mon, 24 Aug 2026 16:52:22 -0300 Subject: [PATCH] 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 --- logic/classification.js | 2 +- logic/classification.test.js | 5 ++++ logic/dataQuality.js | 29 +++++++++++++++++++++++ logic/dataQuality.test.js | 46 ++++++++++++++++++++++++++++++++++++ 4 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 logic/dataQuality.js create mode 100644 logic/dataQuality.test.js diff --git a/logic/classification.js b/logic/classification.js index 6592257..a0db42e 100644 --- a/logic/classification.js +++ b/logic/classification.js @@ -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'); diff --git a/logic/classification.test.js b/logic/classification.test.js index 0dcc474..321533d 100644 --- a/logic/classification.test.js +++ b/logic/classification.test.js @@ -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); diff --git a/logic/dataQuality.js b/logic/dataQuality.js new file mode 100644 index 0000000..3c2d464 --- /dev/null +++ b/logic/dataQuality.js @@ -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 }; diff --git a/logic/dataQuality.test.js b/logic/dataQuality.test.js new file mode 100644 index 0000000..31243e4 --- /dev/null +++ b/logic/dataQuality.test.js @@ -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), []); +});