# Scorecards de QA / QA automático (agente.md secao 117-119) Sub-fase C da fase de IA: avalia cada chamada transcrita contra os critérios de qualidade que o próprio tenant define, sem lista fixa hardcoded (secao 117 sugere Saudação/Identificação/Empatia/etc. como exemplo, nunca como enum). ## Modelo de dados (já existia desde a migration `ai_module`) - `QualityScorecard` — um conjunto de critérios, por tenant, com `enabled` (soft delete, mesmo padrão do resto do sistema). - `QualityScorecardItem` — um critério: `name`, `weight`, `description?`, `evaluationPrompt?`. - `QualityEvaluation` — o resultado de avaliar UMA chamada contra UM scorecard: `score` (0-100), `criterionScores` (Json, mapa nome-do-critério → 0-100), `summaryJustification`. **Nunca** guarda o chain-of-thought do modelo (secao 118, explícito) — só o resultado final validado. ## CRUD `apps/api/src/quality/quality-scorecards.controller.ts` — mesma RLS por tenant do resto do sistema (sem hierarquia GLOBAL/BYOK aqui, scorecard é sempre do tenant). `POST /quality/scorecards` cria o scorecard e seus itens numa tacada só (nested create); `GET` lista os habilitados; `DELETE /:id` desabilita (soft delete). ## Pipeline (`apps/ai-worker/src/process-scorecard.ts`) Novo `AIJobType.SCORECARD_EVALUATION` (migration `20260828194146_ai_job_scorecard_evaluation`, só `ALTER TYPE ... ADD VALUE`, sem mudança de RLS). Encadeado a partir de `process-transcription.ts` **junto** com o job de ANALYSIS — mesma decisão de privacidade (precisa de `allowsAnalysis`), só que também exige pelo menos 1 `QualityScorecard` habilitado pro tenant (senão nem cria o job). Ao processar: busca TODOS os scorecards habilitados do tenant (não só o primeiro) e gera uma `QualityEvaluation` por scorecard. O prompt é montado dinamicamente a partir dos itens de cada scorecard (`buildScorecardPrompt`) — o JSON Schema mandado pro provider (`QUALITY_EVALUATION_JSON_SCHEMA`, `packages/ai/src/ quality-evaluation-schema.ts`) só define a FORMA da resposta (score + mapa de criterionScores), não as chaves específicas, já que os critérios variam por scorecard. O texto da transcrição passa pelo `SensitiveDataRedactor` antes de sair, igual à análise normal. ## Dashboard de IA (`GET /reports/ai-dashboard`, secao 119) Agrega `CallAIAnalysis` + `QualityEvaluation` do período: chamadas analisadas, score médio (`avgQualityScore`, de `CallAIAnalysis` — quão bem o modelo achou que a ligação foi — E `avgScorecardScore`, de `QualityEvaluation` — a nota formal contra os scorecards; a especificação só pede "score médio" sem dizer qual dos dois conceitos, então mostra os dois em vez de escolher um), sentimento, principais assuntos/objeções (contagem de frequência sobre os arrays já persistidos), compliance alerts, ranking de agentes por `agentScore` médio (top 5 / bottom 5). ## O que foi testado de verdade Mesma restrição de rede desta sessão (só o servidor git é autorizado). Testado ao vivo contra o `b2bcall-ai-worker` real e Postgres real com RLS: scorecard real criado com 2 critérios via `QualityScorecardsController` (nested create), `processScorecardJob` chamado diretamente contra uma transcrição semeada — montou o prompt a partir dos itens reais, resolveu provider, redigiu o texto, tentou a chamada de rede real (loopback fechado), falhou como esperado; job `SCORECARD_EVALUATION` real criado e reservado pelo worker via `FOR UPDATE SKIP LOCKED`, retry com backoff, dead-letter exatamente na tentativa configurada. `GET /reports/ai-dashboard` e `POST /quality/scorecards` confirmados respondendo (401 sem token, 200 esperado com token — não testado com um usuário autenticado real nesta rodada, mesmo padrão de outras fases onde o roteamento/guard já foi provado em fases anteriores). **Nunca exercitado**: chamada de rede real contra OpenAI/Anthropic (mesma restrição de todo o módulo de IA); `avgScorecardScore` calculado a partir de uma `QualityEvaluation` real (só via dead-letter, nunca completou sem rede).