> ## 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.

# Campaign Safety Preflight

> Il sistema campaign_safety_audits — un preflight deterministico e auditabile che può bloccare il lancio di una campagna dietro una scala di severità hard-block.

Prima della v0.18.16, i controlli di sanità delle campagne erano ad-hoc — export CSV e script una tantum eseguiti manualmente prima di un invio rischioso. **Il Campaign safety preflight** (BORD-699/700/701) sostituisce questo approccio con un record di database durevole e auditabile di cosa è stato controllato, quando, e su quale esatto insieme di destinatari — e una protezione al lancio **opt-in** che può rifiutare di lanciare una campagna il cui preflight non è passato.

<Info>
  Il preflight è disattivato per impostazione predefinita. Le campagne esistenti vengono lanciate esattamente come prima, a meno che i metadati di una campagna non abilitino esplicitamente l'opt-in tramite `launchSafety.requireSafetyPreflight`.
</Info>

## Cosa fa un'esecuzione di preflight

`runCampaignSafetyPreflightInSupabase` esegue un unico passaggio deterministico sui destinatari **in coda** (`queued`) di un batch di campagna:

1. Risolve le campagne nell'ambito — sia ogni campagna che condivide una `batchKey` (una "famiglia" di campagne, associata tramite `campaigns.metadata->>'batchKey'`), sia un elenco esplicito di id di campagna.
2. Carica i `campaign_recipients` in coda per quelle campagne, uniti alle colonne dei contatti rilevanti per la sicurezza (`consent_state`, `subscriber_status`, `deleted_at`, `deletion_requested_at`, `metadata`).
3. Apre una riga di audit (`campaign_safety_audits`, stato `running`).
4. Esegue ogni gate deterministico registrato sull'insieme di destinatari e raccoglie i finding.
5. Persiste i finding (`campaign_safety_audit_findings`) e completa l'audit — scrivendo un `recipient_set_hash` sugli id ordinati dei destinatari e i conteggi di severità aggregati, quindi porta lo stato a `completed`.
6. In caso di qualsiasi errore dopo l'apertura della riga di audit, l'audit viene marcato `failed` con il motivo dell'errore, e l'errore viene ri-lanciato — un preflight non riporta mai silenziosamente successo in caso di crash.

### Gate rilasciato: DNC

L'unico gate attivo oggi è **`dncGate`** — un controllo autonomo basato esclusivamente sui dati Switchbord. Segnala un destinatario come do-not-contact (severità `dnc`, tipo di finding `dnc_opted_out`) quando una qualsiasi delle seguenti condizioni è vera, registrando quali di esse hanno avuto riscontro come evidenza:

* `consent_state === "opted_out"`
* `subscriber_status === "unsubscribed"`
* il contatto ha `deleted_at` impostato
* il contatto ha `deletion_requested_at` impostato

<Note>
  L'adapter ha un **punto di estensione** documentato per gate con identità espansa (Travio/Neo4j/UDB prior-outreach, riscontri sullo status di viaggio, fuzzy matching sui pax — tracciati come BORD-702/703/704/705). Questi gate necessitano di un contesto risolto per identità più ricco di quello fornito dal solo insieme di destinatari e sono intenzionalmente fuori ambito per questo preflight iniziale.
</Note>

## Scala di severità

I finding hanno una delle sette severità, dalla più alla meno grave:

| Severità         | Blocca il lancio?                                                                                                              | Significato                                                                                                                                      |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `hard_block`     | **Sì** — qualsiasi finding hard-block fa fallire la protezione al lancio                                                       | Riservata ai gate con identità espansa (non ancora implementati) che rappresenterebbero un invio certo a qualcuno che non deve essere contattato |
| `medium_review`  | No — evidenziata per revisione dell'operatore                                                                                  | Riservata a gate futuri                                                                                                                          |
| `dnc`            | No — evidenziata per revisione dell'operatore, ma ogni destinatario coinvolto è uno che questo preflight prevede venga escluso | La severità del gate DNC rilasciato                                                                                                              |
| `prior_outreach` | No                                                                                                                             | Riservata a gate futuri                                                                                                                          |
| `status_hit`     | No                                                                                                                             | Riservata a gate futuri                                                                                                                          |
| `pax_fuzzy`      | No                                                                                                                             | Riservata a gate futuri                                                                                                                          |
| `info`           | No                                                                                                                             | Finding informativi a formato libero; non conteggiati in nessuna colonna di aggregazione                                                         |

<Warning>
  Solo i finding `hard_block` bloccano il lancio oggi. Il gate DNC rilasciato usa deliberatamente `dnc`, non `hard_block` — un audit completato con solo finding DNC supera comunque la protezione al lancio. Trattare i finding DNC come un segnale di revisione per l'operatore, non come un blocco al lancio, finché BORD-706 (modalità apply-withholds) non viene rilasciato.
</Warning>

## La protezione al lancio

Una campagna abilita l'opt-in alla protezione tramite i propri metadati (`launchSafety.requireSafetyPreflight: true`) oppure facendo passare esplicitamente `requireSafetyPreflight: true` alla chiamata di lancio da parte del chiamante. Quando abilitata, `launchCampaignInSupabase` rifiuta di lanciare a meno che **tutte** le seguenti condizioni non siano vere:

1. Esiste un audit **completato** per la `batchKey` di questa campagna (o per l'id di campagna, se non c'è una batch key).
2. Il `hard_block_count` di quell'audit è esattamente zero.
3. Il `recipient_set_hash` dell'audit corrisponde a un hash **live** ricalcolato sui destinatari in coda attuali della campagna al momento del lancio.

Se qualsiasi controllo fallisce, `launchCampaign` lancia un'eccezione con un motivo specifico (`no completed campaign safety preflight audit found`, `... has N hard-block finding(s)`, oppure `recipient set changed since audit ...; rerun preflight`) e segna il tentativo di lancio come fallito — non lancia mai parzialmente.

```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
```

### Perché esiste il controllo hash

Il `recipient_set_hash` è un digest con prefisso `sha256:` degli id ordinati dei `campaign_recipient` in coda effettivamente coperti dall'audit. Ricalcolarlo e confrontarlo al momento del lancio chiude una lacuna evidente: se i destinatari vengono aggiunti o modificati *dopo* l'esecuzione del preflight (un'audience più ampia, un batch rimaterializzato, una modifica manuale di un destinatario), l'audit è una prova obsoleta e non deve essere considerata attendibile. La protezione forza una nuova esecuzione del preflight piuttosto che lanciare contro un'audience che in realtà nessuno ha controllato.

<Info>
  Per un audit su `batchKey`, l'hash viene calcolato sull'**unione di tutti i destinatari in coda dell'intera famiglia di batch** — corrispondendo alla popolazione che il preflight stesso ha sottoposto a hash. Un audit su id di campagna calcola l'hash solo sui destinatari in coda di quella specifica campagna. La protezione risolve la stessa popolazione usata dall'audit prima di effettuare il confronto, così i due hash sono sempre comparabili tra loro.
</Info>

### Progettato per fallire in modo sicuro (fail-closed)

La protezione al lancio è stata irrobustita contro alcuni specifici percorsi di elusione durante una revisione adversariale:

* Il flag di abilitazione `requireSafetyPreflight` viene letto dai **metadati JSON grezzi della campagna**, non tramite lo schema analizzato/validato — così un campo di metadati malformato e non correlato altrove nella campagna non può mai disabilitare silenziosamente il gate.
* L'hash live di un audit su `batchKey` viene ricalcolato sull'*intera famiglia di batch*, non su una singola campagna, quindi non può mai corrispondere banalmente a una popolazione più ristretta.
* Un audit **vuoto** o un insieme di destinatari live vuoto viene rifiutato apertamente, piuttosto che essere considerato passato "in modo vacuo" — un preflight su nulla non è prova che qualcosa sia stato controllato.

## Modello dei dati

Due tabelle con ambito workspace, protette da RLS (tramite `app.has_workspace_access`):

**`campaign_safety_audits`** — una riga per ogni esecuzione di preflight.

| Colonna                                                                                                               | Note                                                                                                                                        |
| --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `batch_key` / `campaign_ids`                                                                                          | Almeno uno popolato — o una famiglia di batch o un insieme esplicito di id di campagna                                                      |
| `mode`                                                                                                                | `audit_only` (predefinito — produce finding, non tocca nulla) o `apply_withholds` (BORD-706, non ancora implementato)                       |
| `status`                                                                                                              | `running` → `completed` o `failed`; uno stato del ciclo di vita, non un verdetto di successo/fallimento                                     |
| `recipient_set_hash` / `recipient_count`                                                                              | Impostato al completamento; guida il controllo di obsolescenza della protezione al lancio                                                   |
| `hard_block_count`, `medium_review_count`, `dnc_count`, `prior_outreach_count`, `status_hit_count`, `pax_fuzzy_count` | Aggregati dai finding al completamento (ricalcolati dalla tabella dei finding, non tracciati incrementalmente, per evitare disallineamenti) |
| `config` / `summary` / `artifact_paths`                                                                               | Config dell'esecuzione a formato libero e payload del report per l'operatore; nessun dato PII inline                                        |

**`campaign_safety_audit_findings`** — N righe per audit, una per ogni riscontro (destinatario, tipo di finding).

| Colonna                                | Note                                                                                                                                    |
| -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `severity`                             | Uno dei sette valori nella scala sopra                                                                                                  |
| `finding_type`                         | Testo libero (es. `dnc_opted_out`) così i gate futuri possono aggiungere tipi senza una migrazione                                      |
| `campaign_recipient_id` / `contact_id` | Il destinatario/contatto a cui si riferisce il finding                                                                                  |
| `matched_travio_id` / `match_methods`  | Evidenza di identity-expansion per gate futuri                                                                                          |
| `evidence`                             | Evidenza strutturata di cui un operatore ha bisogno per agire sul finding (es. quali campi di consent/subscriber hanno avuto riscontro) |
| `confidence`                           | `high` / `medium` / `low`, per i gate fuzzy                                                                                             |

## Eseguire un preflight

Non esiste ancora una superficie UI dedicata — `runCampaignSafetyPreflightInSupabase(admin, { workspaceId, batchKey | campaignIds, mode?, config? })` viene chiamato direttamente dal tooling operativo prima di un lancio protetto. Passa una `batchKey` (preferibile per una famiglia multi-campagna) oppure un array esplicito `campaignIds`; la chiamata lancia un'eccezione se nessuno dei due è utilizzabile, così un audit non può mai essere avviato contro una popolazione illimitata o ambigua.

<Tip>
  Esegui nuovamente il preflight ogni volta che i destinatari cambiano dopo un'esecuzione precedente — la protezione al lancio rifiuterà comunque un audit obsoleto, ma ri-eseguirlo in modo proattivo evita un tentativo di lancio fallito al momento del go-live.
</Tip>

## Triage

| Sintomo                                                                               | Causa probabile                                                                                     | Cosa fare                                                                                                                                    |
| ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| Il lancio genera l'eccezione `no completed campaign safety preflight audit found`     | `requireSafetyPreflight` è attivo ma non è mai stato eseguito alcun audit per questa campagna/batch | Esegui `runCampaignSafetyPreflightInSupabase` per il batch/campagna, poi ritenta il lancio                                                   |
| Il lancio genera l'eccezione `... has N hard-block finding(s)`                        | Un gate hard-block è scattato (possibile solo una volta rilasciati i gate con identità espansa)     | Rivedi `campaign_safety_audit_findings` per quell'id di audit e risolvi i destinatari segnalati prima di ritentare                           |
| Il lancio genera l'eccezione `recipient set changed since audit ...; rerun preflight` | I destinatari sono stati aggiunti/modificati dopo l'esecuzione dell'audit                           | Ri-esegui il preflight sull'insieme in coda attuale, poi ritenta il lancio                                                                   |
| Il lancio genera l'eccezione `... or live recipient set is empty`                     | O l'audit ha coperto zero destinatari o la campagna attualmente ha zero destinatari in coda         | Conferma che la campagna abbia destinatari in coda; ri-esegui il preflight su un insieme non vuoto                                           |
| Esistono finding DNC ma il lancio riesce comunque                                     | Previsto — DNC non è una severità `hard_block`                                                      | Rivedi manualmente i finding; il dispatch verifica separatamente il consenso al momento dell'invio e salta i contatti opted-out in ogni caso |

## Vedi anche

* [Broadcast Engine](/it/operations/broadcasts) — dove il preflight si inserisce nel ciclo di vita del lancio di una campagna
* [Futura Campaign Go-Live Testing](/it/operations/futura-campaign-testing) — la checklist di preflight manuale esistente che questo sistema è progettato per formalizzare in futuro
* [Platform → Data Model](/it/platform/data-model) — convenzioni delle tabelle con ambito workspace
