Pular para conteúdo

Contrato e coleta de métricas sociais

Responsabilidade: persistir e validar snapshots sociais, coletar pontos iniciais/semanais, calcular comparação semanal versionada por canal, materializar aprendizados aceitos, produzir score geral explicável contra o histórico próprio, expor essa leitura em /app/performance e consumir os aprendizados somente em novas ideias. Paths: supabase/migrations/20260820190000_social_analytics_contract_dev1647.sql, supabase/migrations/20260820213000_initial_social_metric_collection_dev1648.sql, supabase/migrations/20260821223000_weekly_social_analysis_dev1649.sql, supabase/migrations/20260821234500_social_account_score_dev1650.sql, supabase/migrations/20260826130000_accepted_social_channel_learning_dev1837.sql, supabase/migrations/20260827170000_versioned_social_editorials_dev1660.sql, supabase/migrations/202608272{00000_harden_editorial_strategy_expand,10000_harden_editorial_strategy_contract}_dev1660.sql, src/lib/social-analytics-contract.ts, src/lib/social-metrics-collector.ts, src/lib/social-initial-analysis.ts, src/lib/social-weekly-analysis.ts, src/lib/social-channel-learning.ts, src/lib/social-metric-registry.ts, src/lib/social-account-score.ts, src/lib/social-performance*.ts, src/app/api/social/analytics/{initial,weekly,score}/route.ts, src/components/app/PerformanceDashboard/, cadencia-workers/src/shared/social_channel_learning.py, cadencia-workers/src/workers/{ideas,editorial_strategy}.py, cadencia-workers/src/api/routes/{ideas,cron}.py Stack: PostgreSQL/Supabase, RLS, TypeScript, Zod, Luxon, Next.js e Composio como provider de transporte Issues: DEV-1647, DEV-1648, DEV-1649, DEV-1650, DEV-1652, DEV-1658, DEV-1837, DEV-1659 e DEV-1660

O que faz

A DEV-1647 evolui a tabela legada e vazia post_performance para social_metric_snapshots, preservando as colunas antigas e adicionando um contrato versionado, provider-neutral e rastreável. Snapshots podem representar perfil ou post; análises são initial ou weekly; aprendizados são versionados por canal e ficam vinculados a uma versão do dossier.

A DEV-1648 acrescenta a primeira coleta real. Depois de uma conexão social ser confirmada, o callback agenda a coleta inicial com after(). O endpoint autenticado POST /api/social/analytics/initial recupera execuções falhas ou com lease expirado. O coletor busca métricas disponíveis no provider, normaliza cada canal e conclui uma execução transacional no banco. Instagram coleta perfil e até 25 mídias; LinkedIn pessoal coleta perfil e associa somente publicações já conhecidas em content_publications, sem inventar zero quando uma métrica não está disponível.

As migrations DEV-1647, DEV-1648 e DEV-1649 estão aplicadas em produção. A execução controlada da DEV-1648 em 2026-08-21 gravou 30 snapshots: 26 de Instagram e 4 de LinkedIn. O rollout da DEV-1649 entrou no master pelo PR #358 e foi promovido pela Vercel no deployment dpl_4WYHbxdoVreJTMfwHW3gi8xqt9ZF, com SHA bc451a940482a4705b92451c2ff9714d341c0ff5 no alias cadencia.app.br. O E2E controlado acrescentou 30 snapshots semanais, duas análises e 30 vínculos de evidência, sem criar publicação social. As análises ficaram unavailable, como esperado sem baseline semanal elegível.

A DEV-1650 está no master produtivo. O score é recalculado depois da persistência de cada análise semanal; falha do scorer fica auditada em run próprio e não desfaz nem invalida a análise de canal. A primeira execução produtiva controlada persistiu score unavailable, resultado esperado enquanto não existe baseline semanal comparável.

A DEV-1837 conecta a produção determinística à fronteira de leitura da DEV-1658. Análises iniciais e semanais persistidas materializam uma versão accepted por conexão, canal, schema e dossier vigente. Migration, deploy e canário produtivo foram concluídos em 26/08/2026; o read-back confirmou materialização idempotente e zero publicação social.

Fluxo da coleta inicial

  1. O servidor resolve tenant_id, user_id, conexão, canal e provider explicitamente.
  2. claim_initial_social_metric_collection cria ou reutiliza a execução idempotente e entrega um lease com token de execução.
  3. O coletor consulta o provider sem publicar nem alterar a conta social.
  4. Perfil e posts são normalizados para o contrato Zod do canal; ausência de dado vira cobertura partial ou unavailable, nunca falso zero.
  5. persist_initial_social_metric_collection grava snapshots e finaliza a execução sob o mesmo token. Falhas usam fail_initial_social_metric_collection; lease expirado pode ser retomado com segurança.

Fluxo da análise semanal

  1. POST /api/social/analytics/weekly autentica o usuário, resolve o tenant e seleciona somente uma conexão ativa pertencente àquele usuário/tenant/canal.
  2. A execução falha fechado fora da segunda-feira em America/Sao_Paulo. Como as métricas do provider são cumulativas, coletar em outro dia misturaria a semana corrente na fotografia.
  3. claim_social_metric_collection usa collection_kind=weekly e chave weekly:v1:<window_start>. Retry da mesma janela reutiliza a coleta; a API inicial continua compatível por wrappers finos.
  4. O mesmo adapter Composio da coleta inicial cria a fotografia semanal. persist_social_metric_collection aloca snapshot_version sob lock da conexão.
  5. O analisador compara a coorte atual com a última coleta terminal concluída em até ±24h da segunda inicial. A janela é a última segunda-domingo completamente fechada; a anterior é contígua. Coletas tardias não sombreiam um baseline semanal elegível.
  6. Cada métrica carrega currentValue, previousValue, delta e deltaPercent. Divisão por zero ou ausência de um lado produz null, não infinito nem zero inventado.
  7. persist_weekly_social_channel_analysis grava análise e evidências na mesma transação. O fingerprint das evidências torna o retry idempotente; evidência nova na mesma janela cria nova analysis_version.

Cobertura

  • complete: as duas coortes são comparáveis e todas as evidências usadas são completas.
  • partial: existe comparação, mas algum snapshot/provider declarou lacuna.
  • unavailable: falta baseline elegível ou não existe nenhuma métrica comparável. O payload preserva os valores correntes observados, mas mantém valores anteriores, deltas, wins e risks vazios e recomenda uma nova coleta.

Score geral explicável

O registry social-metric-registry.ts é a fonte única da semântica de cada métrica: dimensão, tipo, agregação, direção e elegibilidade. Rates usam média. Contadores de perfil usam a diferença entre fotografias. Contadores de post são pareados por source_external_id antes da soma; posts sem par ficam explicitamente excluídos, nunca entram como zero. following e mediaCount permanecem contexto.

O scorer usa somente Instagram e LinkedIn da própria conta social, identificada por provider_account_id. Reconectar a mesma conta preserva as janelas históricas: primeiro é escolhida a maior analysis_version dentro de cada conexão e janela; entre conexões da mesma conta, vence a análise canônica mais recente por generated_at e analysis_id. A análise da janela corrente precisa pertencer à conexão ativa, porque ela é a evidência que dispara e ancora a nova versão do score. Trocar de conta começa outro baseline. São consideradas no máximo oito janelas semanais anteriores contíguas. Análise unavailable, semana ausente e métrica sem valor semanal não pontuam.

Para cada métrica elegível:

  • performance = percentil do valor atual contra os valores históricos elegíveis;
  • tendência = clamp(50 + variação percentual, 0, 100), com regra explícita para baseline zero;
  • métrica = 50% performance + 50% tendência;
  • dimensão = média das métricas; canal = média das dimensões; conta = média dos canais elegíveis.

Canal ausente nunca reduz a nota. Ele reduz somente a confiança: cobertura de canais × profundidade histórica × qualidade dos dados. Cobertura complete, partial e unavailable valem respectivamente 1, 0.6 e 0. Menos de três valores históricos por métrica gera status provisional; três ou mais gera available; sem métrica pontuável retorna unavailable e score nulo.

social_account_score_runs, social_account_scores e social_account_score_analysis_evidence possuem tenant_id, user_id, RLS de leitura própria e escrita somente por RPC service_role. Claims e persistência aceitam somente janelas de exatamente sete dias. A persistência recebe toda inputEvidence, trava o horizonte fixo W…W-8, reconstrói sob lock o conjunto histórico contíguo esperado, relê a versão canônica de cada análise e rejeita score_input_stale inclusive quando uma janela antes ausente aparece. Score e evidência são append-only. O fingerprint ordenado cobre analysis_id:analysis_version da janela corrente e de todas as janelas históricas contíguas carregadas, inclusive inputs depois excluídos da pontuação; isso favorece rastreabilidade sobre deduplicação mínima. A explicação preserva esse lineage completo em inputEvidence, enquanto a tabela relacional ancora apenas as análises correntes validadas sob lock.

Valores brutos e agregados usam números finitos, não necessariamente não negativos: métricas como ganho de seguidores podem representar perda. Rates continuam limitadas a 0..1; deltas e contadores direcionais preservam sinal. Com baseline zero, tendência positiva recebe impulso, zero permanece neutro e perda recebe score zero após clamp.

O lock advisory usa epoch UTC da janela, nunca a representação textual dependente do timezone da sessão. A persistência trava primeiro as conexões em ordem determinística com FOR NO KEY UPDATE e depois as nove janelas fixas, da mais antiga à corrente. Isso segue a ordem conexão → advisory da RPC semanal, bloqueia disconnect/update e continua compatível com o FOR KEY SHARE das FKs, eliminando ciclos em inserts concorrentes de análise. A cardinalidade SQL de uma a duas evidências correntes é deliberadamente acoplada ao escopo V1 (Instagram e LinkedIn); o conjunto completo aceita até dezoito inputs (dois atuais + oito históricos por canal). Adicionar canal exige migration coordenada do contrato e da guarda runtime, não apenas editar o registry TypeScript. Se todas as conexões desaparecerem antes do recálculo, a run falha explicitamente com SCORE_NO_ACTIVE_CHANNELS sem tentar persistir evidência vazia.

Benchmark anonimizado da base Cadência e integração futura com Social Blade pertencem à DEV-1792. Nenhum benchmark externo entra na fórmula V1.

Fronteira de aprendizado para workers — DEV-1658

cadencia-workers/src/shared/social_channel_learning.py é a fronteira Python provider-neutral sobre social_channel_learnings. O loader exige tenant_id UUID explícito, resolve a única linha vigente de tenant_dossier e lê apenas aprendizados accepted, schema v1 e versão exata do dossier. O resultado mantém no máximo a maior learning_version por (social_connection_id, channel); conexões diferentes do mesmo canal não são colapsadas.

O read model expõe somente conexão, canal, análise-fonte, versões, textos acionáveis, confiança e data de aceite. Proveniência bruta e o JSON completo do banco não chegam aos prompts. Linha cross-tenant, proposed, superseded, schema futuro, dossier antigo ou payload inválido é rejeitada. Ausência real de dossier/aprendizado retorna coleção vazia; erro HTTP, JSON inválido ou coleção malformada levanta erro técnico para não fingir que o tenant apenas não possui dados.

A DEV-1658 não conecta o loader ao gerador de ideias. Em 26/08/2026, o E2E read-only pós-correções executou cinco leituras na conta interna do Felipe, com 11/11 checks e contagem invariável, e confirmou social_channel_learnings = 0 no banco produtivo. Portanto o caminho vazio está validado em produção.

Materialização de aprendizados aceitos — DEV-1837

runInitialSocialAnalysis e runWeeklySocialAnalysis chamam o mesmo produtor depois de persistir a análise. O produtor transforma somente métricas e evidências já validadas em textos acionáveis determinísticos, calcula fingerprint estável e chama materialize_accepted_social_channel_learning. Ele não usa LLM, não atribui causalidade não observada, não altera o dossier e não publica conteúdo.

A RPC roda em uma única transação e sob lock da conexão e do dossier. Ela valida tenant, usuário, conexão, canal, schema v1, cobertura, análise-fonte, evidência e proveniência. Retry do mesmo analysis_id + schema_version reutiliza a linha existente; análise nova incrementa learning_version, marca a aceita anterior como superseded e cria uma única nova linha accepted. Índices únicos impedem dois aceitos concorrentes para a mesma conexão/canal/schema.

Cobertura unavailable não cria aprendizado: o resultado explícito é materialized=false. Cobertura partial ou complete exige evidência válida e ao menos um aprendizado acionável. Falha de materialização é fail-closed para o ciclo social — ele não pode ser declarado concluído sem o aprendizado durável — enquanto a atualização auxiliar do score permanece best-effort.

O callback OAuth e o endpoint de retry inicial usam o orquestrador completo. Uma coleta terminal antiga sem análise ou aprendizado não é tratada como concluída: a rota repara o estado derivado sem duplicar snapshots. O worker da DEV-1658 passa então a encontrar somente a maior versão aceita compatível com o dossier atual.

Consumo em novas ideias — DEV-1659

As gerações on-demand (POST /api/v1/ideas/generate) e diária (POST /api/v1/cron/daily) carregam os aprendizados aceitos pelo loader compartilhado antes de chamar a LLM. Cada conta permanece separada dentro do canal; UUIDs, proveniência bruta e payloads do provider não entram no prompt. Instagram, LinkedIn e TikTok possuem seções determinísticas.

O prompt trata os aprendizados como dados, limita cada texto acionável e declara o dossier como restrição superior. Se não houver dossier ou aprendizado vigente, o bloco não é adicionado e o comportamento anterior é preservado. Se a leitura falhar tecnicamente, a rota on-demand retorna 503 SOCIAL_LEARNINGS_UNAVAILABLE antes da LLM/insert; o cron marca somente aquele tenant como falho e continua os demais. Nada reprocessa ideias antigas, altera posts ou publica conteúdo.

Em 26/08/2026, a DEV-1659 entrou no master pela PR #384, merge commit 31551aa3d90d2cac974e29786d0c547402cf7b8f. Antes do merge, testes focados, comparação contra o baseline, build, lint, reviews e E2E real no Preview passaram: cinco gerações persistiram 40 ideias pending, consumiram três aprendizados aceitos por chamada e mantiveram filas, documentos e publicações em zero; as fixtures e o worker privado foram removidos. No canário produtivo, o worker no SHA exato consumiu um aprendizado aceito do LinkedIn, persistiu nove ideias pending e registrou um lançamento ideas_generation, com delta zero em filas, documentos e publicações e sem publicação social. O kill switch da conta interna foi restaurado após a chamada. O deploy frontend da Vercel ficou bloqueado por fair use e permanece no SHA anterior; isso não altera o runtime da DEV-1659, cuja mudança executável está no worker promovido pelo Coolify. A DEV-1660 aplicará a mesma política aos geradores de conteúdo final.

Ajuste automático de editorias — DEV-1660

A DEV-1660 acrescenta uma estratégia editorial versionada. O cron interno POST /api/v1/cron/social-editorials seleciona tenants active ou grace, carrega o dossier vigente, as três editorias ativas e somente aprendizados sociais accepted compatíveis com a mesma versão do dossier. O prompt declara o dossier como restrição superior e exige exatamente uma editoria demonstrativa, uma educativa e uma confrontadora.

O conjunto ativo permanece fixo em três editorias e pesos semânticos por função: demonstrativa 40%, educativa 35% e confrontadora 25%. sort_order é apenas a ordem visual escolhida; reorder_tenant_editorials_v1 atualiza as três editorias e o onboarding em uma transação, e o ajuste social preserva essa ordem. Uma oportunidade nova substitui a editoria menos aderente; nunca cria uma quarta ativa. apply_tenant_editorial_strategy_v1 valida a proveniência, trava o tenant e as linhas, compara o snapshot ativo completo e grava em uma única transação a nova versão, a supersessão das linhas anteriores e as três linhas substitutas. O fingerprint usa dossier e versões dos aprendizados; repetir a mesma evidência não chama a LLM nem cria outra versão.

As editorias antigas são imutáveis para fins históricos: ideias e conteúdos continuam resolvendo o editorial_id original. Somente consultas de conjunto corrente filtram is_active=true. O onboarding usa a mesma RPC para criar ou substituir um conjunto versionado. Contas legadas que não possuam exatamente as três funções canônicas são preservadas e ignoradas pelo cron; normalizá-las exige uma decisão explícita, nunca inferência automática.

A rota não cria ideias, documentos ou publicações sociais. O relatório semanal ainda não exibe a mudança; essa rastreabilidade humana pertence à DEV-1661. O E2E ampliado e a regressão de não reprocessamento pertencem à DEV-1662.

O scan amplo é ordenado por tenant_id e aceita cursor exclusivo. Quando o teto de três chamadas LLM ou o orçamento de 20 segundos interrompe o lote, a resposta devolve nextCursor com o último tenant efetivamente processado. A chamada seguinte continua somente depois desse cursor; tenant_id e cursor são mutuamente exclusivos. Isso evita reiniciar sempre nas primeiras contas quando há muitos tenants elegíveis e permite avançar mesmo quando um lote inteiro termina em skips sem LLM.

O rollout é expand/contract: migration base + expand compatível com o worker antigo; promoção e smoke do worker/frontend aprovados; contract somente após health real; por fim, canário tenant-scoped e replay. A guarda expand transforma o DELETE + INSERT anterior em substituição sem perda histórica e permanece instalada como fail-safe. Depois da primeira versão, recuperação funcional é roll-forward para a imagem aprovada, nunca rollback ao worker antigo que não filtra is_active.

Em 27/08/2026, o Preview isolado recebeu a migration e o worker foi validado no SHA 8924e12800bca94f6e46f4ef34a69d952625728e. O E2E HTTP criou um tenant descartável com dossier, três editorias canônicas e um aprendizado aceito; a rota respondeu 401 sem segredo, aplicou a versão 2 com uma chamada gpt-5.4 e, no replay, retornou already_applied sem nova LLM. O banco confirmou versões 1 e 2, três linhas antigas preservadas como inativas, três novas ativas, funções e pesos 40/35/25, uma transação de custo e zero ideias, documentos, filas, jobs ou publicações. O cleanup autorizado removeu somente o tenant da fixture e terminou com todos os contadores em zero. A imagem foi executada em container descartável porque o slot compartilhado estava reservado pela DEV-1842; os segredos existiram apenas no processo. O runtime persistente do Preview continua sem CRON_SECRET, OPENAI_API_KEY e api_call_logs, uma lacuna de paridade do ambiente, não da rota.

Na primeira promoção produtiva, migration e worker chegaram ao SHA 30025b3dfa0b08e50152a2178e00e07305a8b7c6. O canário amplo interrompeu o rollout porque três janelas repetiram os primeiros 34 de 43 tenants, sem alcançar os nove finais. A guarda desligou SOCIAL_EDITORIALS_ENABLED; o read-back confirmou zero LLM, zero ajuste, zero publicação e todos os contadores produtivos inalterados.

Em 28/08/2026, o roll-forward promoveu o worker f5f37d315190c2618f8cda331821fe70e11f81fc e o scheduler f563a6343884a0cb7096f54ee44089b7f560884c, habilitou SOCIAL_EDITORIALS_ENABLED=1 e manteve um único cron semanal. O novo canário percorreu os 43 tenants em duas invocações e terminou completed: 43 processados, 43 skips, zero falhas, zero chamadas LLM e zero ajustes. Os skips foram 1 already_applied, 18 sem aprendizado aceito, 11 sem dossier e 13 com conjunto ativo não canônico. O baseline permaneceu em 141 editorias ativas, 47 estratégias, 659 ideias, 386 documentos, 829 itens de fila, zero jobs de publicação, 24 publicações de conteúdo, 297 posts publicados e uma transação de custo editorial social. Nenhuma publicação social ocorreu. A execução funcional está validada; o fechamento da DEV-1887 e a promoção da PR de healthcheck dependem do primeiro heartbeat real do cron semanal.

Leitura em /app/performance

A DEV-1652 substitui a pill genérica “Redes sociais” por Instagram e LinkedIn navegáveis. Email mantém o painel existente; Blog e TikTok continuam visíveis e indisponíveis na V1. A área social fica sob um Suspense próprio, portanto loading ou erro social não reinicializa nem derruba Email.

GET /api/social/analytics/score?week=YYYY-MM-DD autentica a sessão, resolve o tenant no servidor e consulta social_account_scores com filtros explícitos de tenant_id, user_id, window_start e window_end. A maior score_version da janela exata é a versão canônica. A rota aceita somente uma das 12 semanas fechadas geradas em America/Sao_Paulo; sem parâmetro, usa a mais recente e a interface grava essa semana na URL.

O endpoint não devolve explanation bruto. O DTO contém score/status/confiança/componentes gerais e, por canal, somente channel, coverageStatus, score e dataQuality, além de missingChannels. Identidade OAuth continua vindo de GET /api/social/connections, evitando uma segunda representação do contrato de conexões.

Estados visíveis:

  • desconectado ou needs_reconnect: CTA usa openSocialConnectionPopup já existente;
  • conectado sem análise: informa que a primeira leitura depende da análise semanal;
  • cobertura unavailable: informa que falta uma semana comparável;
  • score unavailable: mantém nota ausente, nunca exibe zero;
  • score provisional ou available: mostra nota, confiança, período e explicação determinística.

A tela é somente leitura, exceto pelo OAuth explícito iniciado pelo usuário. Ela não publica conteúdo, não dispara coleta, não recalcula score e não altera content_publications. Gráficos, ranking de posts e relatório detalhado por canal pertencem à DEV-1656.

Agregador semanal DEV-1653

O runtime longo vive no cadencia-growth, não em uma função Vercel. Toda segunda-feira ele reivindica uma execução única por tenant/owner/janela, consolida Email, Blog, Instagram, LinkedIn e TikTok e persiste weekly_performance_reports. Blog e TikTok entram como indisponíveis na V1; Email usa a coorte exata do espelho Resend; Instagram e LinkedIn reutilizam as análises e o score desta feature.

POST /api/v1/social/analytics/weekly é a rota interna server-to-server usada pelo worker. Ela exige Bearer derivado de VPS_TRIGGER_SECRET, aceita apenas UUIDs de tenant/usuário/conexão, valida a conexão ativa novamente no banco e chama runWeeklySocialAnalysis. Não aceita identidade de canal/provider fornecida pelo caller e não publica conteúdo.

A tabela do relatório tem RLS de leitura própria, unicidade por tenant/usuário/janela/versão e escrita somente por RPCs service_role. Claim com lease permite retry após crash; relatório concluído é reutilizado. O payload canônico é a entrada das DEV-1654, DEV-1655 e DEV-1656. Migration, worker e cron DEV-1653 foram validados em produção em 24/08/2026.

Entrega WhatsApp DEV-1654

weekly_performance_deliveries é o ledger genérico da distribuição semanal. Existe no máximo uma linha por report_id + channel; os canais iniciais são whatsapp e email. O owner autenticado lê somente entregas do próprio tenant/usuário por RLS, enquanto claim, persistência, falha e marcação unknown são RPCs exclusivas de service_role.

Estados: pending → running → sent | failed | unknown. sent exige provider message ID e é imutável. failed representa erro definitivo anterior ao provider e pode ser reivindicado novamente. unknown representa timeout, 5xx ou lease expirado com possível efeito externo e bloqueia retry automático.

O destinatário é users.phone filtrado pelo mesmo tenant_id + user_id do relatório. No WhatsApp, o ledger armazena apenas os quatro últimos dígitos; canais não telefônicos usam hint nulo. O remetente é a instância Lara do tenant central configurado no growth, conforme ADR-0020; nenhum número vive no código.

Decisões arquiteturais

  • social_metric_collection_runs mantém idempotência e estado compartilhado no banco; memória do processo não decide concorrência.
  • O provider é proveniência, não identidade do domínio. Canal, sujeito, janela, versão e métricas continuam válidos se o transporte mudar.
  • FKs compostas incluem tenant_id e channel; service_role não substitui os filtros explícitos do tenant.
  • RLS permite somente leitura própria ao usuário autenticado. As RPCs de escrita são restritas ao papel server-side e validam tenant, usuário, conexão, token e lease.
  • Uma coleta parcial preserva os snapshots obtidos e registra os motivos ausentes; falha de um recurso não apaga evidência válida de outro.
  • LinkedIn pessoal não enumera posts arbitrários: apenas publicações canônicas já persistidas são candidatas a snapshot.
  • O callback não bloqueia a resposta OAuth; a coleta assíncrona é observável e repetível pelo endpoint de retry.
  • Coleta inicial e semanal usam o mesmo núcleo/provider adapter. RPCs iniciais são wrappers retrocompatíveis da implementação genérica; regras de transporte não foram duplicadas.
  • O analisador é determinístico e provider-neutral. Não usa LLM, não atribui causalidade editorial e não calcula o score geral da DEV-1650.
  • Uma análise é append-only por janela/versão. Retry só reutiliza quando analyzer, versão, janela e fingerprint das evidências coincidem.
  • A distribuição é separada da agregação: relatório concluído não prova mensagem enviada; somente weekly_performance_deliveries.status='sent' com provider ID prova entrega.
  • Aprendizados permanecem subordinados e versionados; a análise os materializa contra a versão atual do dossier, mas nunca sobrescreve tenant_dossier.

Gotchas e armadilhas

  • Aplicar a migration DEV-1648 antes da DEV-1647 falha por dependência explícita do contrato social.
  • partial exige ao menos um motivo; complete exige a lista vazia.
  • Cobertura unavailable exige motivo e payload vazio; complete/partial exigem ao menos uma métrica.
  • Rates usam fração normalizada de 0 a 1, nunca percentual de 0 a 100.
  • O Composio pode retornar objetos aninhados e paginações diferentes por toolkit. O adaptador percorre registros iterativamente e limita a quantidade processada.
  • Retry não significa duplicação: a chave idempotente e o claim no banco reutilizam uma execução ativa, parcial ou concluída. partial é um baseline terminal com evidência preservada; uma nova janela/versionamento pertence às stories seguintes.
  • A rota semanal aceita coleta somente na segunda-feira BRT. Essa trava é de integridade temporal, não agenda: o cron/relatório semanal pertence à Etapa 3.
  • A janela é meio-aberta no banco: window_start inclusivo e window_end exclusivo. A exibição humana continua sendo segunda a domingo.
  • deltaPercent usa percentual (20 = 20%), diferente das rates normalizadas do snapshot (0.2 = 20%).
  • O limite de 25 posts pertence ao adapter de coleta. Truncamento e indisponibilidade entram em coverage_reasons; a análise não apresenta cobertura completa quando o provider declarou lacuna.
  • Um claim semanal ainda running nunca dispara análise nem persistência; a execução concorrente falha fechado com erro de domínio e a dona do lease conclui o ciclo.
  • Snapshots são append-only por versão. Disconnect atualiza a conexão; não remove o histórico social referenciado.
  • DEV-1649 cria análises semanais e evidências, mas não cria score, learning, relatório, cron ou feedback de worker.
  • DEV-1659 injeta o read model somente na geração de ideias futuras; a geração de conteúdo final continua fora do escopo até a DEV-1660.
  • unavailable é ausência explícita de aprendizado, não versão aceita vazia. partial e complete precisam de evidência e textos acionáveis válidos.
  • O índice de um único accepted é por conexão e canal. Contas diferentes do mesmo canal continuam independentes e não são colapsadas.
  • O score DEV-1650 não publica conteúdo, não altera content_publications, não escreve learning/dossier e não usa benchmark de outro cliente.
  • A rota de leitura DEV-1652 usa janela meio-aberta e exige os dois limites exatos. Chave fora da allowlist retorna 400 INVALID_WEEK antes de consultar scores.
  • score = null é ausência de nota. Qualquer coerção para zero na API ou no componente viola o contrato.
  • Analyzer v1 não entra no score: rates e coortes antigas não têm semântica segura. O baseline do score começa nas análises v2.
  • Não declarar rollout da DEV-1649 concluído apenas porque build e PostgreSQL isolado passaram. O gate exige migration, deployment READY/PROMOTED, execução real do provider e read-back. Esses passos foram cumpridos em 2026-08-21; a rota HTTP continua aceitando agendamento apenas na segunda-feira BRT, e a comparação só deixa de ser unavailable quando houver baseline semanal elegível.

Como validar

O gate autenticado da DEV-1652 roda apenas no Preview isolado, compara a interface com o DTO real e falha se houver qualquer request de mutação em /api/social:

npm run qa:dev1652:performance
npx vitest run src/lib/social-analytics-contract.test.ts src/lib/social-metrics-collector.test.ts src/lib/social-weekly-analysis.test.ts src/lib/social-weekly-analysis.integration.test.ts src/app/api/social/analytics/weekly/route.test.ts tests/integration/weekly-social-analysis-migration.test.ts tests/integration/weekly-social-analysis-postgres.test.ts
npx vitest run src/lib/social-metric-registry.test.ts src/lib/social-account-score.test.ts src/lib/social-account-score.integration.test.ts tests/integration/social-account-score-migration.test.ts tests/integration/social-account-score-postgres.test.ts
npx vitest run src/lib/social-performance-period.test.ts src/app/api/social/analytics/score/route.test.ts src/components/app/PerformanceDashboard/SocialPerformancePanel.test.tsx src/components/app/PerformanceDashboard/PerformanceDashboard.test.tsx
npx vitest run src/lib/social-initial-analysis.test.ts src/lib/social-channel-learning.test.ts src/lib/social-weekly-analysis.test.ts src/app/api/social/analytics/initial/route.test.ts src/app/api/social/analytics/weekly/route.test.ts tests/integration/weekly-social-analysis-postgres.test.ts
npx eslint src/lib/social-analytics-contract.ts src/lib/social-metrics-collector.ts src/lib/social-weekly-analysis.ts src/app/api/social/analytics/weekly/route.ts tests/integration/weekly-social-analysis-postgres.test.ts
cd cadencia-workers && python -m pytest tests/test_social_channel_learning.py tests/test_layout_metadata_helpers.py -q
bash tests/integration/dev1648-real-postgres-e2e.sh
bash tests/integration/dev1649-real-postgres-e2e.sh
npm test -- --run
npm run build

O teste PGlite aplica as migrations DEV-1647→1648→1649 e DEV-1837 em PostgreSQL isolado, cria dois tenants e prova coleta semanal, evidências, idempotência, compatibilidade inicial, tenant scope, grants, materialização aceita, concorrência, supersessão monotônica e zero publicações. O script Docker repete o contrato em PostgreSQL 17 quando Docker está disponível e faz skip explícito quando não está. No E2E produtivo anterior, Instagram e LinkedIn foram executados cinco vezes cada no tenant interno: a primeira passagem persistiu 26 e 4 snapshots, respectivamente, e as quatro seguintes reutilizaram os mesmos runs e análises (attempt_count=1 por canal). O read-back confirmou duas análises, 30 evidências e zero content_publications; cinco chamadas HTTP autenticadas por canal, feitas fora da segunda-feira, retornaram 409 OUTSIDE_WEEKLY_COLLECTION_WINDOW sem mutar cookies. O canário da DEV-1837 materializou e releu versões aceitas de forma idempotente, sem publicação social. Para a DEV-1659, rodar também cd cadencia-workers && python -m pytest tests/test_ideas_social_learning.py tests/test_social_channel_learning.py tests/test_ideas_auto_approve.py tests/test_ideas_ai_disabled.py -q.

Referências

  • Linear: DEV-1647, DEV-1648, DEV-1649, DEV-1650, DEV-1652, DEV-1658, DEV-1837 e DEV-1659; parents DEV-1625, DEV-1626 e DEV-1627
  • RFC-003 — Integração Social via Composio
  • docs/features/social-connections/index.md
  • docs/adr/0010-composio-oauth-unico.md
  • docs/adr/0019-publicacao-social-assincrona-estado-por-canal.md