feat(v2): normalize GLPI text before context/hash, add real ticket description

context.normalizeTicket now runs normalizeGlpiText on titulo, descricao
(new field, sourced from glpi_tickets.content -- never fetched before),
and every followup/task texto. buildAiContextText restructured into
delimited sections (TITULO / DESCRICAO ORIGINAL / STATUS-TECNICO-GRUPO /
FOLLOW-UPS / TAREFAS) instead of one flat string.

hash.buildContextFingerprint now includes titulo/descricao and the
normalized followup/task texto (previously only id/date/status), so:
(a) the same content encoded differently (HTML vs escaped entities)
produces the same hash, and (b) editing a followup's actual content
(same id/date) is now detected as a real context change -- previously
silently invisible to the hash.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Gustavo
2026-08-24 16:47:32 -03:00
co-authored by Claude Sonnet 5
parent d7650308a9
commit db7a261a10
4 changed files with 106 additions and 29 deletions
+7 -3
View File
@@ -13,11 +13,15 @@ function djb2Hash(str) {
return hash.toString(16).padStart(8, '0');
}
const FINGERPRINT_FIELDS = ['status', 'tecnico_atribuido', 'categoria', 'entidade'];
const FINGERPRINT_FIELDS = ['status', 'tecnico_atribuido', 'categoria', 'entidade', 'titulo', 'descricao'];
// Campos textuais (titulo, descricao, followups[].texto, tasks[].texto) entram no fingerprint
// já normalizados (normalizeTicket roda normalizeGlpiText antes disso) -- por isso a mesma
// informação escrita com HTML puro ou com entidades escapadas produz o mesmo hash. Nunca
// inclui dias_aberto/dias_sem_interacao/idade: o hash só muda por evento real de contexto.
function buildContextFingerprint(ticket) {
const followups = (ticket.followups || []).map((f) => `${f.id}:${f.date}`).sort();
const tasks = (ticket.tasks || []).map((t) => `${t.id}:${t.status}:${t.date}`).sort();
const followups = (ticket.followups || []).map((f) => `${f.id}:${f.date}:${f.texto || ''}`).sort();
const tasks = (ticket.tasks || []).map((t) => `${t.id}:${t.status}:${t.date}:${t.texto || ''}`).sort();
const picked = {};
for (const field of FINGERPRINT_FIELDS) picked[field] = ticket[field];
picked.followups = followups;