// @requires dataQuality.js // Este node roda depois de "Merge - Unir Shadow" (append: Code - Montar Digest Coordenacao + // MySQL - Legado Top 10 (Shadow)), que existe so para GARANTIR que os dois ja executaram -- // os dados continuam sendo lidos por nome, nao pelo item mesclado, para nao depender de ordem. const digestData = $('Code - Montar Digest Coordenacao').first().json; // { mensagem, selected, excluded, dateStr, digest, changedBlocked } const legacyRows = $('MySQL - Legado Top 10 (Shadow)').all().map((item) => item.json); const legacyIds = legacyRows.map((row) => Number(row.id)); const v2Ids = digestData.selected.map((item) => Number(item.ticket.id)); const added = v2Ids.filter((id) => !legacyIds.includes(id)); const removed = legacyIds.filter((id) => !v2Ids.includes(id)); const addedReasons = added.map((id) => { const item = digestData.selected.find((s) => Number(s.ticket.id) === id); return { id, motivo: item ? `${item.aiClassification.classification} - ${item.aiClassification.reason}` : 'desconhecido' }; }); const removedReasons = removed.map((id) => { const excludedItem = digestData.excluded.find((e) => Number(e.ticket.id) === id); return { id, motivo: excludedItem ? (excludedItem.exclusionReason || excludedItem.aiClassification.classification) : 'nao estava nos candidatos V2' }; }); // --- observabilidade (secao 24 da missao) --- const allItems = [...digestData.selected, ...digestData.excluded]; const cacheHits = allItems.filter((i) => i.resolucao === 'cache').length; const aiRequested = allItems.filter((i) => i.resolucao === 'ia').length; const aiFallbackCapCount = allItems.filter((i) => i.resolucao === 'fallback_cap').length; const aiSuccess = allItems.filter((i) => i.resolucao === 'ia' && !i.aiClassification.is_fallback).length; const aiErrors = allItems.filter((i) => i.resolucao === 'ia' && i.aiClassification.is_fallback).length; const aiFallbackCount = allItems.filter((i) => i.aiClassification.is_fallback).length; const aiFallbackByReason = {}; allItems .filter((i) => i.aiClassification.is_fallback) .forEach((i) => { const r = i.aiClassification.fallback_reason || 'unknown'; aiFallbackByReason[r] = (aiFallbackByReason[r] || 0) + 1; }); const classificationCounts = {}; allItems.forEach((i) => { const c = i.aiClassification.classification; classificationCounts[c] = (classificationCounts[c] || 0) + 1; }); const actionableRealCount = allItems.filter((i) => !i.aiClassification.is_fallback && (i.aiClassification.classification === 'ACTIONABLE_NOW' || i.aiClassification.classification === 'UNASSIGNED_ACTIONABLE')).length; const criticalOverrideCount = allItems.filter((i) => i.decisionReason && String(i.decisionReason).endsWith('_critical_override')).length; const observability = { candidate_count: allItems.length, cache_hits: cacheHits, cache_misses: allItems.length - cacheHits, ai_requested: aiRequested, ai_success: aiSuccess, ai_errors: aiErrors, ai_fallback_count: aiFallbackCount, ai_fallback_cap_count: aiFallbackCapCount, ai_fallback_by_reason: aiFallbackByReason, classification_counts: classificationCounts, insufficient_context_count: classificationCounts.INSUFFICIENT_CONTEXT || 0, actionable_real_count: actionableRealCount, critical_override_count: criticalOverrideCount, top10_count: digestData.selected.length, blocked_count: digestData.excluded.length, coordination_count: classificationCounts.NEEDS_COORDINATION || 0, }; const dataQualityReview = buildDataQualityReview(allItems); return [{ json: { run_at: Date.now(), legacy_ids: JSON.stringify(legacyIds), v2_ids: JSON.stringify(v2Ids), added: JSON.stringify(added), removed: JSON.stringify(removed), summary: JSON.stringify({ addedReasons, removedReasons, observability, dataQualityReview }), mensagem: digestData.mensagem, digest: digestData.digest, }, }];