> ## Documentation Index
> Fetch the complete documentation index at: https://docs.switchbord.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Preflight de Segurança de Campanha

> O sistema campaign_safety_audits — um preflight determinístico e auditável que pode bloquear o lançamento de campanhas atrás de uma escada de severidade de hard-block.

Antes da v0.18.16, as verificações de sanidade de campanha eram ad-hoc — exportações de CSV e scripts pontuais executados manualmente antes de um envio de risco. O **preflight de segurança de campanha** (BORD-699/700/701) substitui isso por um registro de banco de dados durável e auditável do que foi verificado, quando, e contra qual conjunto exato de destinatários — e uma trava de lançamento **opt-in** que pode se recusar a lançar uma campanha cujo preflight não passou.

<Info>
  O preflight é desativado por padrão. Campanhas existentes são lançadas exatamente como antes, a menos que os metadados de uma campanha explicitamente ativem o opt-in via `launchSafety.requireSafetyPreflight`.
</Info>

## O que uma execução de preflight faz

`runCampaignSafetyPreflightInSupabase` executa uma única passagem determinística sobre os destinatários **enfileirados** de um batch de campanhas:

1. Resolve as campanhas dentro do escopo — ou todas as campanhas que compartilham uma `batchKey` (uma "família" de campanhas, correlacionada via `campaigns.metadata->>'batchKey'`), ou uma lista explícita de ids de campanha.
2. Carrega os `campaign_recipients` enfileirados dessas campanhas, unidos (joined) às colunas de contato relevantes para segurança (`consent_state`, `subscriber_status`, `deleted_at`, `deletion_requested_at`, `metadata`).
3. Abre uma linha de audit (`campaign_safety_audits`, status `running`).
4. Executa todos os gates determinísticos registrados sobre o conjunto de destinatários e coleta os findings.
5. Persiste os findings (`campaign_safety_audit_findings`) e completa o audit — gravando um `recipient_set_hash` sobre os ids de destinatários ordenados e as contagens de severidade consolidadas, então muda o status para `completed`.
6. Em caso de qualquer erro após a abertura da linha de audit, o audit é marcado como `failed` com o motivo do erro, e o erro é relançado (re-thrown) — um preflight nunca reporta sucesso silenciosamente em caso de falha (crash).

### Gate disponível: DNC

O único gate ativo hoje é o **`dncGate`** — uma verificação autocontida, baseada apenas em dados do Switchbord. Ele marca um destinatário como do-not-contact (severidade `dnc`, tipo de finding `dnc_opted_out`) quando qualquer uma destas condições é verdadeira, e registra quais delas corresponderam como evidência:

* `consent_state === "opted_out"`
* `subscriber_status === "unsubscribed"`
* o contato tem `deleted_at` definido
* o contato tem `deletion_requested_at` definido

<Note>
  O adapter tem um **ponto de extensão** documentado para gates de identidade expandida (Travio/Neo4j/UDB de contato prévio, hits de status de viagem, correspondência fuzzy de pax — rastreados como BORD-702/703/704/705). Esses gates precisam de contexto de identidade resolvida mais rico do que apenas o conjunto de destinatários fornece, e estão intencionalmente fora do escopo deste preflight inicial.
</Note>

## Escada de severidade

Os findings carregam uma de sete severidades, da mais para a menos grave:

| Severidade       | Bloqueia o lançamento?                                                                                                      | Significado                                                                                                                                          |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `hard_block`     | **Sim** — qualquer finding hard-block reprova a trava de lançamento                                                         | Reservado para os gates de identidade expandida (ainda não implementados) que representariam um envio definitivo a alguém que não deve ser contatado |
| `medium_review`  | Não — exibido para revisão do operador                                                                                      | Reservado para gates futuros                                                                                                                         |
| `dnc`            | Não — exibido para revisão do operador, mas todo destinatário infrator é alguém que este preflight espera que seja excluído | A severidade do gate DNC disponível                                                                                                                  |
| `prior_outreach` | Não                                                                                                                         | Reservado para gates futuros                                                                                                                         |
| `status_hit`     | Não                                                                                                                         | Reservado para gates futuros                                                                                                                         |
| `pax_fuzzy`      | Não                                                                                                                         | Reservado para gates futuros                                                                                                                         |
| `info`           | Não                                                                                                                         | Findings informativos de formato livre; não contados em nenhuma coluna de rollup                                                                     |

<Warning>
  Apenas findings `hard_block` bloqueiam o lançamento hoje. O gate DNC disponível deliberadamente usa `dnc`, não `hard_block` — um audit completado apenas com findings DNC ainda passa pela trava de lançamento. Trate findings DNC como um sinal de revisão do operador, não como um bloqueador de lançamento, até que o BORD-706 (modo apply-withholds) seja lançado.
</Warning>

## A trava de lançamento

Uma campanha ativa o opt-in para a trava via seus próprios metadados (`launchSafety.requireSafetyPreflight: true`) ou por quem chama a função passando explicitamente `requireSafetyPreflight: true` na chamada de lançamento. Quando habilitada, `launchCampaignInSupabase` se recusa a lançar a menos que **todas** as condições abaixo sejam válidas:

1. Existe um audit **completado** para a `batchKey` desta campanha (ou id da campanha, se não houver batch key).
2. O `hard_block_count` desse audit é exatamente zero.
3. O `recipient_set_hash` do audit corresponde a um hash **em tempo real (live)** recalculado sobre os destinatários enfileirados atuais da campanha no momento do lançamento.

Se qualquer verificação falhar, `launchCampaign` lança uma exceção com um motivo específico (`no completed campaign safety preflight audit found`, `... has N hard-block finding(s)`, ou `recipient set changed since audit ...; rerun preflight`) e marca a tentativa de lançamento como falha — ela nunca lança parcialmente.

```text theme={null}
launchCampaign (requireSafetyPreflight = true)
  1. resolve batchKey or campaignId
  2. load LIVE queued campaign_recipients ids for that population
  3. fetch latest COMPLETED audit for the same population
  4. evaluateCampaignLaunchGuard(audit, liveQueuedIds):
       - no audit                         → BLOCK
       - audit.hardBlockCount > 0         → BLOCK
       - live set empty OR audit count 0  → BLOCK (a preflight of nothing proves nothing)
       - sha256(sorted live ids) ≠ audit.recipientSetHash → BLOCK (stale — rerun)
       - otherwise                        → PASS, launch proceeds
```

### Por que a verificação de hash existe

O `recipient_set_hash` é um digest prefixado com `sha256:` dos ids de `campaign_recipient` enfileirados e ordenados que o audit efetivamente cobriu. Recalcular e comparar esse hash no momento do lançamento fecha uma lacuna óbvia: se destinatários forem adicionados ou alterados *depois* que o preflight foi executado (uma audiência maior, um batch rematerializado, uma edição manual de destinatário), o audit é evidência obsoleta e não deve ser confiado. A trava força uma nova execução de preflight em vez de lançar contra uma audiência que ninguém de fato verificou.

<Info>
  Para um audit de `batchKey`, o hash é calculado sobre a **união de todos os destinatários enfileirados em toda a família de batch** — correspondendo à população que o próprio preflight hasheou. Um audit por id de campanha hasheia apenas os destinatários enfileirados daquela campanha específica. A trava resolve a mesma população que o audit usou antes de comparar, de modo que os dois hashes são sempre comparáveis entre si (apples-to-apples).
</Info>

### Fail-closed por design

A trava de lançamento foi reforçada contra alguns caminhos de contorno específicos durante a revisão adversarial:

* A flag de habilitação `requireSafetyPreflight` é lida a partir do **JSON bruto de metadados da campanha**, não através do esquema analisado/validado — de modo que um campo de metadados malformado e não relacionado em outro lugar da campanha nunca pode desabilitar silenciosamente o gate.
* O hash em tempo real de um audit de `batchKey` é recalculado sobre a *família de batch inteira*, não sobre uma única campanha, de modo que nunca pode corresponder trivialmente a uma população mais estreita.
* Um audit **vazio** ou um conjunto de destinatários em tempo real vazio é recusado diretamente, em vez de passar "vacuamente" — um preflight de nada não é evidência de que algo foi verificado.

## Modelo de dados

Duas tabelas com escopo de workspace, protegidas por RLS (via `app.has_workspace_access`):

**`campaign_safety_audits`** — uma linha por execução de preflight.

| Coluna                                                                                                                | Notas                                                                                                                                                   |
| --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `batch_key` / `campaign_ids`                                                                                          | Ao menos um preenchido — ou uma família de batch, ou um conjunto explícito de ids de campanha                                                           |
| `mode`                                                                                                                | `audit_only` (padrão — produz findings, não altera nada) ou `apply_withholds` (BORD-706, ainda não implementado)                                        |
| `status`                                                                                                              | `running` → `completed` ou `failed`; um estado de ciclo de vida, não um veredito de aprovação/reprovação                                                |
| `recipient_set_hash` / `recipient_count`                                                                              | Definido na conclusão; direciona a verificação de obsolescência da trava de lançamento                                                                  |
| `hard_block_count`, `medium_review_count`, `dnc_count`, `prior_outreach_count`, `status_hit_count`, `pax_fuzzy_count` | Consolidados a partir dos findings na conclusão (recalculados a partir da tabela de findings, não rastreados incrementalmente, para evitar divergência) |
| `config` / `summary` / `artifact_paths`                                                                               | Configuração de execução em formato livre e payload de relatório do operador; sem PII inline                                                            |

**`campaign_safety_audit_findings`** — N linhas por audit, uma por hit de (destinatário, tipo de finding).

| Coluna                                 | Notas                                                                                                                                  |
| -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `severity`                             | Um dos sete valores da escada acima                                                                                                    |
| `finding_type`                         | Texto livre (ex.: `dnc_opted_out`) para que gates futuros possam adicionar tipos sem uma migração                                      |
| `campaign_recipient_id` / `contact_id` | Sobre qual destinatário/contato o finding trata                                                                                        |
| `matched_travio_id` / `match_methods`  | Evidência de expansão de identidade para gates futuros                                                                                 |
| `evidence`                             | Evidência estruturada que um operador precisa para agir sobre o finding (ex.: quais campos de consentimento/subscriber corresponderam) |
| `confidence`                           | `high` / `medium` / `low`, para gates fuzzy                                                                                            |

## Executando um preflight

Ainda não há uma superfície dedicada na UI — `runCampaignSafetyPreflightInSupabase(admin, { workspaceId, batchKey | campaignIds, mode?, config? })` é chamado diretamente a partir de ferramentas operacionais antes de um lançamento protegido pela trava. Passe uma `batchKey` (preferível para uma família com múltiplas campanhas) ou um array explícito de `campaignIds`; a chamada lança uma exceção se nenhum dos dois for utilizável, de modo que um audit nunca pode ser iniciado contra uma população não delimitada ou ambígua.

<Tip>
  Execute o preflight novamente sempre que os destinatários mudarem após uma execução anterior — a trava de lançamento vai rejeitar um audit obsoleto de qualquer forma, mas executar novamente de forma proativa evita uma tentativa de lançamento falha no momento do go-live.
</Tip>

## Triagem

| Sintoma                                                                     | Causa provável                                                                                               | O que fazer                                                                                                                                                            |
| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| O lançamento lança `no completed campaign safety preflight audit found`     | `requireSafetyPreflight` está ativado, mas nenhum audit foi executado ainda para esta campanha/batch         | Execute `runCampaignSafetyPreflightInSupabase` para o batch/campanha e tente o lançamento novamente                                                                    |
| O lançamento lança `... has N hard-block finding(s)`                        | Um gate hard-block foi disparado (só é possível uma vez que os gates de identidade expandida forem lançados) | Revise `campaign_safety_audit_findings` para o id daquele audit e resolva os destinatários marcados antes de tentar novamente                                          |
| O lançamento lança `recipient set changed since audit ...; rerun preflight` | Destinatários foram adicionados/alterados após a execução do audit                                           | Execute o preflight novamente contra o conjunto enfileirado atual e tente o lançamento de novo                                                                         |
| O lançamento lança `... or live recipient set is empty`                     | Ou o audit cobriu zero destinatários, ou a campanha atualmente tem zero destinatários enfileirados           | Confirme que a campanha tem destinatários enfileirados; execute o preflight novamente contra um conjunto não vazio                                                     |
| Findings DNC existem, mas o lançamento ainda é bem-sucedido                 | Esperado — DNC não é uma severidade `hard_block`                                                             | Revise os findings manualmente; o dispatch reverifica o consentimento separadamente no momento do envio e ignora contatos que fizeram opt-out, independentemente disso |

## Veja também

* [Broadcast Engine](/pt-BR/operations/broadcasts) — onde o preflight se encaixa no ciclo de vida de lançamento de uma campanha
* [Teste de Go-Live de Campanha Futura](/pt-BR/operations/futura-campaign-testing) — o checklist manual de preflight existente que este sistema foi projetado para eventualmente formalizar
* [Platform → Modelo de Dados](/pt-BR/platform/data-model) — convenções de tabelas com escopo de workspace
