fix(v2): fix cache is_fallback bug, wire observability + data quality, drop debug webhook, fix GLPI URL

- Real bug found by code review while implementing this cycle: "Code -
  Reusar Classificacao em Cache" always hardcoded is_fallback: false when
  reconstructing a classification from memory, even when the cached
  classification was itself AI_FALLBACK. On a cache hit for a previously
  fallback-classified ticket, this would have let it skip
  applyFallbackPolicy's exclusion entirely (the policy checks
  aiClassification.is_fallback first). Fixed to derive is_fallback from
  the cached ai_classification value.
- select-top10.node.js now carries `resolucao` (ia | cache | fallback_cap)
  through to the scored item, previously dropped -- needed to tell cache
  hits apart from real AI calls in observability.
- Code - Comparar Legacy x V2 now computes the full observability summary
  (candidate_count, cache_hits/misses, ai_requested/success/errors,
  ai_fallback_count and by-reason breakdown, classification_counts,
  insufficient_context_count, actionable_real_count,
  critical_override_count, top10_count, blocked_count,
  coordination_count) and the data-quality review (buildDataQualityReview
  over INSUFFICIENT_CONTEXT tickets), both stored in the shadow_runs
  `summary` JSON column -- no new Data Table needed.
- Removed the debug webhook entirely (Debug - Ver Painel, Data Table -
  Buscar Shadow Runs Recentes debug branch, Code - Montar Painel Debug,
  Debug - Responder) and the now-unused debug-panel.node.js driver.
  Workflow went from 37 to 33 nodes. Debugging goes through n8n execution
  data, the Data Tables, and the shadow diff instead, as requested.
- Fixed glpi_base_url: was still V1's own placeholder
  (https://glpi.suaempresa.com.br, never a real value, not report
  anonymization) -- confirmed the real domain (https://glpi.gtigrupo.com.br)
  from the live, active GLPI x WhatsApp workflow's own GLPI API config in
  the same n8n instance. SQL still only ever receives it through
  {GLPI_BASE_URL} substitution from the Config node, never hardcoded.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Gustavo
2026-08-24 19:50:48 -03:00
co-authored by Claude Sonnet 5
parent 485234dc88
commit 3c87ab55e1
7 changed files with 101 additions and 177 deletions
+51 -1
View File
@@ -1,3 +1,4 @@
// @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.
@@ -19,6 +20,55 @@ const removedReasons = removed.map((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(),
@@ -26,7 +76,7 @@ return [{
v2_ids: JSON.stringify(v2Ids),
added: JSON.stringify(added),
removed: JSON.stringify(removed),
summary: JSON.stringify({ addedReasons, removedReasons }),
summary: JSON.stringify({ addedReasons, removedReasons, observability, dataQualityReview }),
mensagem: digestData.mensagem,
digest: digestData.digest,
},