# AgilePixel — contratto dell'API (versione 1.1)

> **AgilePixel** è la **vista sovrana** di Agile: legge documenti, bollette, foto e immagini e
> ne estrae i campi in **JSON**. Questo è il contratto: versionato, con gli **errori con nome**,
> e con un `/salute` che dice la verità anche quando è sgradevole.
>
> Contratto in forma di macchina: [`/openapi.json`](/openapi.json), copia versionata nel repo in
> `contratto/openapi-1.1.json`. **Se i due non coincidono, la prova del passo P5 fallisce**: il
> contratto non scivola in silenzio. La copia del `1.0` — `contratto/openapi-1.0.json` — **resta
> nel repo e non si cancella**: è il patto di chi è già cliente, ed è il metro con cui
> `prove/p89` misura che il `1.1` sia un **soprainsieme** del `1.0` e non un `2.0` travestito.
>
> Stato di oggi (30/9/2026): **il motore GPU non esiste ancora**. Il fronte è in piedi, sa
> smistare e lo dichiara; finché non c'è un motore, `/health` risponde `stato: degradato`,
> `motore: "nessuno"` e le chiamate di lettura tornano `503 nessun_motore_disponibile`.
> Non c'è nessun finto «ok» in questo servizio.

## Indice
1. [Come si accede](#1-come-si-accede) — licenze e chiavi
2. [Leggere ed estrarre](#2-leggere-ed-estrarre) — l'API di prodotto
3. [Salute del servizio](#3-salute-del-servizio) — `/salute`, e `/health` che resta
4. [Contratto dell'amministratore](#4-contratto-dellamministratore-adminlicenze)
5. [Console del tenant](#5-console-del-tenant)
6. [Modulo pubblico «richiedi una licenza»](#6-modulo-pubblico-richiedi-una-licenza)
7. [ERRORI CON NOME](#7-errori-con-nome) — l'elenco completo
8. [Limiti e regole](#8-limiti-e-regole)

---

## 1. Come si accede

**I due nomi sono due servizi diversi.** Le API stanno **solo** su
`api.agilepixel.agile.software`; su `agilepixel.agile.software` c'è la landing, la documentazione,
`/health` e il modulo pubblico, e nient'altro: `/v1`, `/admin` e `/console` su quel nome
**non esistono** (`404 rotta_sconosciuta`, non un 403). Un nome che non è nessuno dei due riceve
`404 nome_sconosciuto`.

La forma è quella comune a tutti i servizi sovrani di Agile (kb/52 §3bis):

```
TENANT (il cliente che paga; i suoi clienti sono sotto-tenant)
   └── LICENZA  (piano, ambiti, quota mensile, limite al minuto, scadenza, stato)
          └── CHIAVI API  (N per licenza; ruotarne una non tocca la licenza)
```

- l'**uso si misura per licenza** (pagine e immagini): è la riga che AgileBilling fatturerà;
- il **valore di una chiave si vede una volta sola**: in deposito c'è solo l'impronta `scrypt`;
- **ruotare** una chiave lascia alla vecchia **24 ore di grazia**, poi smette;
- gli **ambiti** sono `leggi`, `estrai`, `descrivi`: una chiave non può averne più della sua licenza.

Ogni chiamata all'API di prodotto porta la chiave nell'intestazione:

```bash
curl -X POST https://api.agilepixel.agile.software/v1/documenti/leggi \
  -H "Authorization: Bearer apx_xxxxxxxxxxxx_..." \
  -F documento=@bolletta.pdf
```

I **piani** hanno quota e limite; il **prezzo è «su richiesta»** (i prezzi sono del titolare).

| Piano | Quota mensile (pagine + immagini) | Limite al minuto | Durata | Prezzo |
|---|---|---|---|---|
| `prova` | 500 | 10 | 30 giorni | su richiesta |
| `standard` | 20 000 | 60 | 365 giorni | su richiesta |
| `enterprise` | 500 000 | 300 | 365 giorni | su richiesta |

Il **limite al minuto** si conta a *minuto d'orologio* (`int(adesso() // 60)`), non su una
finestra che scorre: a cavallo di due minuti possono passare fino al doppio delle richieste
dichiarate in pochi secondi. Quota e limite al minuto sono **tetti su di te, non posti
riservati per te**: dicono quanto il servizio accetta *da questa licenza*, non quanta capacità ti
tiene da parte. I posti di lavoro (in volo e in coda) sono **del servizio** e si dividono con tutti
gli altri clienti — vedi §8. Gli **ambiti** sono identificatori del contratto e non si traducono:
`leggi`, `estrai`, `descrivi`.

*Questi numeri sono la **proposta del box alla regia** (punto 6 di `docs/PIANO.md`): quota e
limite devono stare nel deposito, quindi qualcuno doveva scriverli. La regia li corregge e si
cambiano in un posto solo (`servizio/licenze.py`, `PIANI`).*

**Licenze firmate** (Ed25519, verifica offline, per i servizi installati dal cliente):
**non esistono in questa versione** — sospese dal titolare il 30/9/2026.

---

## 2. Leggere ed estrarre

### `POST /v1/documenti/leggi` — ambito `leggi`
Documento → **markdown** con le tabelle. Consuma **una pagina di quota per pagina letta**.

| Campo | Tipo | Note |
|---|---|---|
| `documento` | file (multipart) | PDF, PNG, JPEG, TIFF, WEBP. Il tipo si riconosce dal **contenuto**, non dal nome del file. |

Le **pagine le conta il fronte**, non il motore: `pagine` in risposta è il numero contato
all'ammissione — lo stesso che è stato pesato contro la quota e lo stesso che finisce in fattura.
Se un motore ne dichiara un numero diverso, vince il nostro e la differenza finisce nel log.

**Il confine fra una facciata e l'altra** dentro `markdown` è una riga orizzontale di markdown,
`\n\n---\n\n`: lo stesso segno qualunque motore ti abbia servito il documento. Dove il
servizio **non** sa dirti il confine non lo inventa — un motore che legge due facciate in una
volta sola ti dà un pezzo solo — quindi i pezzi fra due separatori sono **al massimo** quante
sono le pagine, non esattamente. Se ti serve una facciata per volta, manda una facciata per
volta.

**Un pezzo VUOTO fra due separatori è una facciata da cui non è uscito niente** — tipicamente una
facciata scansionata in un documento per il resto digitale. Non è un errore e non è un pezzo da
buttare: è una **posizione**, e il servizio non la rifila nemmeno quando capita per prima o per
ultima (quindi il `markdown` può cominciare o finire col separatore). Così il pezzo numero *n* è
sempre la *n*-esima cosa che il servizio ha letto, e le pagine che hai pagato (`pagine`) ti
tornano tutte: se buttassimo i vuoti, dal primo buco in poi i tuoi numeri di facciata sarebbero
sbagliati di uno e nessuno te lo direbbe.

Risposta `200`:
```json
{
  "markdown": "# Fornitore\n\n**pod**: IT001E12345678\n\n| Fascia | Consumo |\n|---|---|\n| F1 | 210.0 |",
  "pagine": 2,
  "ms": 1840,
  "ms_fronte": 1902,
  "motore": "agilepixel-motore-1",
  "lavoro": "lav_7f1ac3503c0c1d2e"
}
```

### `POST /v1/documenti/estrai` — ambito `estrai`
Documento (+ `tipo` noto oppure `schema` JSON) → **campi**, con una **confidenza per campo**.

| Campo | Tipo | Note |
|---|---|---|
| `documento` | file (multipart) | come sopra |
| `tipo` | testo, facoltativo | es. `bolletta_luce`, `bolletta_gas` |
| `schema` | testo (JSON), facoltativo | lo schema dei campi da estrarre; se non è JSON valido → `schema_non_valido` |

#### `schema`: chiedere **solo i campi che ti servono** si paga in meno attesa

Lo `schema` non è soltanto la forma della risposta: è **l'elenco dei campi che chiediamo al
modello**. Il modello scrive un valore e una confidenza **per campo**, e il tempo di una lettura se
ne va quasi tutto **lì** — non a guardare la pagina. Quindi chiedere nove campi invece di
ventinove non è una richiesta più ordinata: è la stessa risposta, e si aspetta **2,5 volte** meno
sui PDF e **1,8 volte** meno sulle foto. I due numeri non sono uguali, e il perché conviene
saperlo: sulle foto una fetta del tempo è il **parser** che legge la pagina (**11,3 s** su 25,6),
e lo `schema` non la tocca — taglia quel che il modello **scrive**, non quante volte lo si chiama.

Misurato il **6/10/2026** contro il motore vero (`Qwen3-VL-8B-Instruct-AWQ-4bit`), sulle **stesse**
pagine, con i **9 campi critici giusti 9 su 9** in tutte e due le configurazioni:

| quel che chiedi | PDF (4 pagine) | foto (3 foto) | gettoni scritti dal modello (PDF) |
|---|---|---|---|
| i 29 campi della bolletta, nessuno `schema` | 17,4 s | 25,6 s | 752 |
| uno `schema` coi 9 campi critici | 7,1 s | 14,5 s | 228 |

Il guadagno viene dal **numero** dei campi, non dalla parola `schema`: uno `schema` che elenca
tutti e ventinove costa come non mandarne nessuno. Le forme accettate sono due e **valgono uguale**
— un elenco di nomi (`["pod","totale_da_pagare"]`) o un oggetto con `properties`. Quel che **non**
cambia è il conto: la quota si misura in **pagine**, non in campi (§8), e chi chiede nove campi
paga la stessa pagina e aspetta meno.

I limiti di questi numeri, detti da noi prima che li scopra tu: sono **4 pagine PDF e 3 foto** del
nostro set — bastano a vedere un fattore due, non sono un set — e il tempo si muove col motore e
con la scheda che gli sta sotto, che **non è ancora quella definitiva**. Le pagine, una per una,
stanno in `misure/dove-va-il-tempo-pdf.tsv` e `misure/dove-va-il-tempo-foto.tsv`;
`prove/p86_chiedere_meno_campi.py` tiene questa tabella **legata** a quei file — ricalcola le medie
e boccia se i due numeri si separano — e misura dal posto tuo che lo `schema` che mandi arriva
davvero fino al modello.

**Una facciata che il motore non riesce a leggere non ti fa perdere il resto del documento.** Un
documento di più facciate viaggia al modello **a gruppi** di facciate (quante per volta lo decide
il motore, non tu), e di un gruppo fatto di sole scansioni il modello può non avere niente da
dire. In quel caso ricevi **200** coi campi che gli **altri** gruppi hanno letto: il POD sta sulla
prima facciata e il totale sull'ultima, e perderli tutti per una pagina illeggibile in mezzo
sarebbe il documento buttato via. Quando **nessun** gruppo ha risposto niente che si possa
leggere, la lettura **non è riuscita** e ricevi un errore, non una risposta vuota. Le pagine le
paghi in tutti e due i casi, perché il modello è stato chiamato su tutte. Quel che il servizio
**non** ti dice qui è *quale* facciata è rimasta muta: se ti serve saperlo chiedi
`/v1/documenti/leggi`, dove il buco si **vede** — è un pezzo vuoto fra due separatori, e tiene il
suo posto.

Fra i campi che il servizio restituisce ce n'è uno che non si misura ma si **usa**: `tariffa`
(`monoraria`, `bioraria`, `trioraria`), letta dal documento quando il documento la dichiara. Serve
a sapere se il controllo sulle fasce orarie **si può fare**: vedi `fasce_mancanti` più sotto. Una
parola che non è una di quelle tre vale «non dichiarata» — non è un errore, e non ha un nome.

Risposta `200`:
```json
{
  "campi": {"pod": "IT001E12345678", "totale_da_pagare": 84.32, "consumo_f2": null},
  "confidenze": {"pod": 0.98, "totale_da_pagare": 0.97, "consumo_f2": null},
  "da_verificare": ["consumo_f2"],
  "motivi": {"consumo_f2": ["somma_fasce_errata"]},
  "pagine": 1, "ms": 2100, "ms_fronte": 2174,
  "motore": "agilepixel-motore-1", "lavoro": "lav_7f1ac3503c0c1d2e"
}
```

**E non c'è niente altro.** Le chiavi di questo blocco sono *tutte* quelle che ricevi: il fronte
costruisce la risposta da un elenco dichiarato (`servizio/forma.py`), non girandoti il JSON del
motore. Prima del 6/10/2026 lo girava, e questa stessa chiamata tornava con **20** chiavi invece di
nove quando dietro c'era il motore vero e **nove** col motore del banco: i due stadi del modello
(`ms_parser`, `chiamate`, `uso_modello`…) uscivano fino al cliente, e fra loro una confidenza
**viva** su un campo che la regola d'oro aveva azzerato. Se un giorno ti servisse uno di quei
numeri, chiedilo: diventa una riga del contratto, con un nome che non cambia quando cambiamo
motore. Quel che **non** facciamo è dartelo per caso.

**Regola che non si negozia**: un campo che non passa i validatori italiani (POD, PDR, codice
fiscale e P.IVA con cifra di controllo, `F1+F2+F3 = totale`, `imponibile + IVA = totale`,
coerenza delle date) torna **`null` con `da_verificare`**. **Mai un valore inventato.** Un importo
sbagliato dato per certo rompe la fiducia più di un campo vuoto.

Come leggere le due chiavi:

| Chiave | Tipo | Che cos'è |
|---|---|---|
| `da_verificare` | lista di nomi di campo, ordinata | I campi che il servizio ha **azzerato**. Ogni nome che compare qui vale `null` in `campi` e `null` in `confidenze`: una confidenza su un valore cancellato non vuol dire niente. |
| `motivi` | `{campo: [nomi di errore]}` | **Perché** quel campo è sparito, con un nome (`somma_fasce_errata`, `iva_non_torna`, `pod_non_valido`, `periodo_rovesciato`…). Gli errori che non colpiscono un campo solo — `consumo_totale_mancante`, `imponibile_mancante`, `spese_mancanti` — stanno sotto la chiave **`documento`**; quelli che hanno un campo (`tipo_documento_sconosciuto`, `pdr_su_bolletta_luce`, `codice_fiscale_controllo_errato`) stanno sotto il **loro** campo. |

Tre cose da sapere, perché si notano appena si prova:
- un campo **azzerato non viene mai corretto**: il servizio dichiara di non sapere, non tira a
  indovinare il valore giusto;
- `motivi` può nominare un campo che **non** è in `da_verificare` (sotto `documento`, o perché
  l'errore ha un nome ma nessun campo da azzerare): è un avviso, non un buco;
- `fasce_mancanti` è **l'avviso**, e si legge così: compare **solo** se la tariffa dichiara le
  fasce (`bioraria`, `trioraria`) e nel documento non se ne trova **nessuna**; lì l'assenza è
  un'informazione e te la diciamo. Dove il documento le fasce **non le prevede** — il **gas**,
  sempre, e la luce con tariffa dichiarata **monoraria** — non compare affatto: è un controllo
  che non si può fare, non un difetto, e un documento corretto non ti torna indietro con un
  motivo. Quando compare non azzera niente: tutti i campi del documento restano al loro posto e
  `da_verificare` resta vuoto. La somma invece si controlla sempre: se una fascia c'è e i conti
  non tornano, il nome è `somma_fasce_errata` e quello **sì** azzera i consumi;
- l'estrazione a **schema libero** (senza i campi di una bolletta) non passa dai validatori
  italiani: `da_verificare` è vuota e `motivi` è `{}`. Non è un via libera, è il servizio che
  non finge di saper validare un documento che non conosce.

#### Il secondo filtro: `confidenza_sotto_soglia` — oggi **spento**

I validatori bocciano quello che l'aritmetica smentisce. Su un campo come `fornitore` o
`mercato` non c'è aritmetica: resta solo la **confidenza** che il motore dichiara. Il servizio
sa azzerare anche per quella — `motivi` dice `confidenza_sotto_soglia`, distinto dai nomi dei
validatori, così si capisce se il campo è caduto perché *non torna* o perché il motore *non era
sicuro* — ma la soglia che governa il filtro **vale 0,0: il filtro è spento**.

Non è una dimenticanza, è una misura. `banco/soglia.py` calcola, per il motore che sta dietro,
il **potere di separazione** della sua confidenza (quanto spesso un campo esatto è dichiarato
con più sicurezza di uno sbagliato: 0,5 = moneta, 1,0 = oracolo) e propone la soglia che toglie
più campi sbagliati pagando meno campi esatti, col metro del mandato — almeno **tre sbagliati
tolti per ogni esatto perso**.

**Col motore vero quella soglia non esiste, ed è misurato.** Il 6/10/2026, su 24 bollette PDF e
24 foto del set di prova, la confidenza che il modello dichiara **non distingue i campi giusti
dai campi sbagliati**: separazione 0,453 sui PDF e 0,496 sulle foto, contro lo 0,6 che serve per
accendere — e 0,5 è il valore di una moneta. Il motivo si vede guardando una risposta: dentro un
documento la confidenza è **un numero solo**, uguale su tutti i campi letti (0,99 oppure 1,0), e
cambia da un documento all'altro. Non ordina niente, quindi non c'è niente da tagliare. Il conto
regge: i campi sbagliati in mano erano **46 campi** sulle foto, sopra il minimo di **30 campi**
che l'attrezzo pretende prima di rispondere.

Per questo il filtro resta spento, e **non è prudenza a metà: una soglia scritta a mano qui è un
interruttore**. A 0,95 — il numero che verrebbe da scrivere — non cade **0 campi** su nessuno dei
48 documenti; mezzo punto più su, a 0,995, la stessa leva **svuota** 9 documenti PDF su 24 e 5
foto su 24 e non tocca gli altri. Lo stesso valore in configurazione, due clienti, due servizi
diversi.

**Una confidenza che varia esiste — ma non è quella che il modello dichiara, ed è il seguito
misurato dello stesso giorno.** Il motore sa dire quanto era probabile ogni parola che ha scritto;
la confidenza del *valore* di un campo è quella del suo pezzo più debole, e per costruzione cambia
da campo a campo invece che da documento a documento. Rimisurata sulle stesse pagine, sulla stessa
popolazione di campi e con lo stesso metro: separazione **0,975** sui PDF (contro 0,453) e **0,898**
sulle foto (contro 0,496), e nessuno dei documenti la porta piatta. **Oggi quel numero non esce da
qui**: la risposta continua a consegnare `confidenze` come è descritto sopra — la confidenza che il
modello dichiara — e il filtro resta spento. Accendere l'altro vuol dire promettere al cliente una
cosa nuova (che cosa quel numero significhi, e con quale garanzia), ed è una decisione che non è
stata ancora presa: quando lo sarà, sarà scritta qui prima che in configurazione.

#### Il terzo filtro: `fogli_in_disaccordo` — **acceso**

Un documento di **quattro fogli o più** non entra in una sola richiesta al motore (il modello ha
un tetto di contesto e un tetto di immagini): viaggia a **gruppi** di pagine, e le risposte si
**fondono** in una. Nella fusione vince, campo per campo, il **primo valore non nullo**
nell'ordine delle pagine — il POD sta sul primo foglio, il totale sull'ultimo, e prendere
«l'ultimo che risponde» perderebbe il primo.

Quando due gruppi rispondono **due valori diversi** per lo stesso campo, uno dei due è sbagliato
e il servizio non ha modo di sapere quale. Allora **non scegliamo**: il campo torna `null`, in
`da_verificare`, con `motivi: ["fogli_in_disaccordo"]`. È un nome distinto dagli altri due
apposta — «due pagine del tuo documento dicono cose diverse» non è «il numero non torna» né «il
motore non era sicuro», e la cosa da fare è un'altra (guardare quel campo sul documento).

Fino al 6/10/2026 il disaccordo veniva **contato e taciuto**: il cliente riceveva il valore del
primo foglio con la sua confidenza intatta e `da_verificare` vuota. Misurato col motore vero su
10 bollette di due fogli mandate a gruppi di uno (`misure/i-fogli-che-si-contraddicono.tsv`):
**3 bollette su 10** hanno portato un disaccordo, **5 campi** in tutto (`letture`,
`partita_iva`, `intestatario_presente`) — e **tutti e 5 i valori consegnati erano sbagliati**,
dichiarati con confidenza **0,99 e 1,0**. Un disaccordo fra due pagine non è un dettaglio
statistico: è il posto dove il valore è sbagliato quasi sempre, e il servizio ne aveva l'indizio
in mano.

Un documento che **non** si spezza — una o due pagine, o un motore che lo prende intero — non
passa da qui: senza due gruppi non c'è disaccordo, e nessun campo si azzera.

#### Il quarto filtro: `tipo_non_promesso` — **acceso**

Ogni campo della bolletta ha un **tipo**, e il tipo è una promessa del contratto come il nome:
**testo** per i quindici `tipo_documento`, `fornitore`, `mercato`, `pod`, `pdr`, `unita_misura`,
`periodo_dal`, `periodo_al`, `data_emissione`, `data_scadenza`, `letture` (`reali`/`stimate`),
`codice_fiscale`, `partita_iva`, `iban`, `tariffa`; **numero** per i tredici importi e consumi
(`consumo_f1`…`consumo_totale`, `potenza_impegnata_kw`, `prezzo_energia`, le tre spese,
`imponibile`, `aliquota_iva`, `importo_iva`, `totale_da_pagare`); **booleano** per
`intestatario_presente`.

Un campo che arriva dal motore in un **altro tipo** torna `null`, in `da_verificare`, con
`motivi: ["tipo_non_promesso"]`. Non lo **raddrizziamo**: `str(valore)` su un booleano arrivato
come frase sarebbe un valore inventato da noi con la firma del servizio sopra, ed è la stessa
regola di sempre — un campo sospetto si azzera, non si aggiusta. L'**assenza** non è un tipo
sbagliato: un campo che il motore non ha trovato resta `null` e non ha motivo.

Perché esiste, misurato il 7/10/2026 col motore vero (`misure/i-fogli-che-si-contraddicono.tsv`):
`intestatario_presente` è tornato `"INTESTATARIO ESEMPIO 3305"` dove si promette `true`, e
`letture` `[{"tipo": "Consumo unico (monorario)", "valore": 1562.0}]` dove si promette `"reali"` —
e il valore arrivava **così**, con confidenza 0,99 e `da_verificare` vuota. Chi legge la risposta
la legge col tipo in mano (`if campi["intestatario_presente"]:` su una stringa piena è **sempre
vero**), e su un **importo** risposto come stringa o come struttura si perde di più del tipo:
l'aritmetica qui sopra **non si può fare** — `"198,00"` non si somma — quindi il controllo che
doveva proteggerti salta, e nessuno te lo dice. Dal posto del cliente, prima e dopo, in
`misure/il-tipo-che-il-contratto-promette.tsv`.

**Quanto costa**, misurato: sul set di prova sano (100 bollette, **2 292** valori) questo filtro
azzera **zero** campi; dal vivo, con un campo storto, ricevi **un** campo in meno — quello storto,
e nient'altro (20 contro 21). Il filtro guarda **solo i nomi che il contratto promette**: su uno
`schema` libero con nomi tuoi non tocca niente, perché su quei tipi non abbiamo promesso nulla.

### `POST /v1/immagini/descrivi` — ambito `descrivi`
Immagine → descrizione. È il ramo «**vista che non identifica**»: il filtro dei dati
particolari (GDPR art. 9) sta nel motore. Consuma **un'immagine di quota**.

| Campo | Tipo | Note |
|---|---|---|
| `documento` | file (multipart) | PNG, JPEG, TIFF, WEBP. Il tipo si riconosce dal **contenuto**, non dal nome del file. |

Risposta `200`:
```json
{
  "descrizione": "Un documento su fondo chiaro con testo e una tabella.",
  "byte": 24180,
  "pagine": 1,
  "ms": 1310,
  "ms_fronte": 1377,
  "motore": "agilepixel-motore-1",
  "lavoro": "lav_7f1ac3503c0c1d2e"
}
```

Questo blocco fino al 6/10/2026 **non c'era**: delle tre rotte a pagamento questa era la sola che
non diceva al cliente che cosa riceve, e quel silenzio si vedeva nel servizio — con un motore
tornavano sette chiavi, con un altro nove. `byte` è la misura di quel che ci hai mandato, e
`pagine` vale **1**: un'immagine è un'immagine, ed è così che va in fattura (§8).

---

## 3. Salute del servizio

### `GET /salute` (e `GET /health`, alias che non scade)
Dal contratto **1.1** la porta della salute si chiama **`/salute`**: i servizi sovrani di Agile si
chiamano in italiano, come AgileChannels e AgileTav. **`/health` resta**, con lo **stesso corpo** e
per sempre: è il nome che il `1.0` promette, e un alias che scade non è un alias. I due nomi non
sono due porte: sono **lo stesso gestore** con due percorsi, e `prove/p89` lo misura chiamandoli
entrambi e pretendendo che differiscano solo nelle chiavi che si muovono **da sé**
(`su_da_secondi`) — se un giorno un campo comparisse su uno solo dei due, quella prova boccia.

```json
{
  "servizio": "agilepixel", "stato": "degradato",
  "perche": ["nessun_motore_pronto"],
  "versione": "1.0.0", "contratto": "1.1", "sha": "244230e",
  "coda": {"in_attesa": 0, "in_volo": 0, "profondita": 0, "serviti": 0, "annullati": 0,
           "lavori_vivi": 0, "max_in_volo": 4, "max_in_attesa": 32},
  "motori": [], "motori_registrati": 0, "motori_pronti": 0, "motore": "nessuno",
  "cadute_totali": 0,
  "deposito": {"pronto": true}, "limiti": {"max_pagine_per_richiesta": 50}
}
```
`stato` vale:
- `ok` — c'è almeno **un motore pronto** e il deposito risponde;
- `degradato` — il fronte è in piedi ma qualcosa manca; `perche` dice **cosa**
  (`nessun_motore_pronto`, `coda_piena`);
- `fermo` — il deposito non risponde.

Il `coda_piena` di `/health` e il `coda_piena` 503 della porta d'ingresso sono **lo stesso
fatto**: i posti d'attesa (`in_attesa` contro `max_in_attesa`, §8) sono finiti e la richiesta
successiva viene rifiutata. Puoi fidarti dell'uno per prevedere l'altro — se `/health` non lo dice,
la tua richiesta entra in coda. *Fino al 3/10/2026 non era vero: `/health` guardava
`in_attesa + in_volo` e la porta guardava `in_attesa`, così per `max_in_volo` riempimenti — 4 su 36
coi numeri di casa, e proprio sotto carico — `/health` si dichiarava `degradato · coda_piena`
mentre serviva. Oggi la parola la decide una riga sola (`Coda.piena`), misurata riempimento per
riempimento da `prove/p57_coda_piena_vuol_dire_una_cosa_sola.py`.*

`motore` è la parola **`nessuno`** quando non c'è nessun motore pronto. Non esiste un `ok` di cortesia.

`cadute_totali` (e `cadute` su ogni motore) contano quante volte un lavoro è caduto su un motore
ed è stato **ripreso da un altro**: vedi la *ricaduta*, qui sotto. Il conto del servizio non torna
indietro quando un motore viene tolto — è la storia del servizio, non lo stato dei motori.

### La ricaduta: cosa succede quando un motore cade

Il fronte smista verso **N motori intercambiabili**. Se il motore scelto cade, il lavoro passa al
**prossimo motore capace**, e il cliente non se ne accorge: finché c'è un motore sano, il guasto
di un altro **non diventa un errore del cliente**. È l'altra metà di «si scala aggiungendo
motori», quella che si vede il giorno in cui un motore a ore muore a metà giro.

Due regole tengono corta la ricaduta:

| Cosa succede al motore | Il fronte | Perché |
|---|---|---|
| `5xx`, connessione rifiutata, motore sparito | **ricade** sul prossimo, e mette quel motore fuori dal giro | è un guasto del motore, non della richiesta |
| `4xx` | **non ricade**: `502 motore_in_guasto` subito, **col `perché` del motore in `dettaglio`** | il motore sta bene, è la richiesta che non va: portarla a tre motori non la fa andare bene e costerebbe tre motori per una richiesta sola |
| tempo scaduto | ricade **solo se resta tempo** | il tempo massimo è **uno per la richiesta**, non uno per motore: chi se lo prende tutto non ne lascia per un secondo tentativo, e il cliente riceve `504 motore_non_risponde` quando l'avrebbe ricevuto comunque |
| `motore_senza_modello` (un motore **senza modello**, cioè che legge solo lo strato testo dei PDF digitali) | **ricade** sul prossimo, e quel motore **resta nel giro** | non è un guasto e non è colpa della richiesta: una foto è un documento che leggiamo, e quel motore non lo sa fare. Se non c'è un motore col modello, il cliente riceve **questo** nome e non `motore_in_guasto` — «vai a cercare un guasto che non c'è» sarebbe falso due volte |
| `200` **senza il carico utile**, o un corpo che **non è JSON** | **ricade** sul prossimo, e quel motore resta nel giro | la forma della risposta è una proprietà del motore, non della richiesta: rimandargliela darebbe la stessa risposta, mandarla al vicino no. Resta nel giro perché toglierlo chiuderebbe il servizio a **tutti** per il documento di **uno** |

Se cadono **tutti**, il cliente riceve l'errore dell'**ultimo** tentativo: «ho provato e non ha
funzionato» è più vero di «non c'era nessuno». Un lavoro servito dopo una ricaduta consuma
**una pagina sola**: ricadere non fa pagare due volte.

**E PRIMA DI RICADERE, il fronte CHIEDE** (dall'8/10/2026). Ogni motore dichiara nella sua salute
non solo le **capacità** (`leggi`, `estrai`, `descrivi`) ma anche i **generi** di documento che
accetta — `pdf`, `immagine` — e il fronte smista su tutte e due le cose: un motore senza modello,
che dichiara `generi: ["pdf"]`, non riceve più la tua foto, e la foto va **diretta** al motore col
modello. Due conseguenze per te, nessuna delle quali cambia una riga del tuo codice: la richiesta
non paga più la chiamata buttata a un motore che aveva già detto di non saperla fare; e se in quel
momento **nessuno** dei motori che sa fare questo lavoro accetta il tuo documento, ricevi
`nessun_motore_disponibile` con un `dettaglio` che dice **quanti** motori sanno fare il lavoro e
che nessuno di loro accetta questo genere — non «nessun motore», che ti manderebbe a cercare un
servizio spento mentre il servizio è su e sta leggendo i PDF di qualcun altro. Quel che la
dichiarazione **non** può dire resta alla ricaduta: un PDF **scansionato** è un `pdf` come gli
altri, e che non si possa leggere senza modello si scopre solo chiedendoglielo — là il nome
`motore_senza_modello` e la ricaduta funzionano come prima. Quel che ogni motore accetta si legge
in `/salute`, nella sua riga di `motori` (campo `generi`).

**Un `200` del motore non è una risposta finché non ha il carico utile.** Il carico utile è la
chiave per cui hai mandato il documento: `markdown` su `/leggi`, `campi` e `confidenze` su
`/estrai`, `descrizione` su `/v1/immagini/descrivi`. Se un motore risponde `200` senza di essa —
o con un corpo che non è JSON — il fronte **non ti consegna quella risposta e non te la
fattura**: ricade su un altro motore, e se non ce n'è ricevi `502 motore_in_guasto` con nel
`dettaglio` **quale** chiave mancava, **zero pagine** di quota consumate. Le altre chiavi della
risposta `200` (`ms`, `byte`) non sono carico utile: se un motore non le manda, la risposta
arriva **senza quelle** — nessuno inventa un numero al posto del motore — e il servizio lo
annota nei suoi registri. Perché questa riga esiste: fino al 6/10/2026 un motore che non mandava
`campi` faceva ricevere `200` e **pagare 3 pagine su 3** per una risposta vuota, e la regola
d'oro (§3) non girava affatto (misurato: `misure/un-200-del-motore-non-e-una-risposta.tsv`,
provato: `prove/p96_un_200_del_motore_non_e_una_risposta.py`).

Il fronte chiede ai motori come stanno **da solo**, ogni pochi secondi, anche se nessuno chiama
`/health`: così un motore morto esce dal giro senza aspettare un cliente che ci sbatta, e un
motore tornato su **rientra da solo**.

### `GET /salute/pronto` (e `GET /health/pronto`, alias che non scade)
`200` solo se `stato` è `ok`, altrimenti **`503`**: serve al proxy, che non deve mandare traffico
a un fronte senza motore. Come sopra, i due nomi sono **un solo gestore**.

---

## 4. Contratto dell'amministratore (`/admin/licenze`)

Intestazione: `X-Amministratore: <gettone>`. È il **contratto comune a tutti i servizi sovrani**
(kb/52 §3bis), così ADP e il futuro store li governano tutti allo stesso modo. La **console**
dell'amministratore Agile è una per tutti i servizi e **non sta in questo box**: qui c'è il contratto.

| Metodo e percorso | Cosa fa |
|---|---|
| `POST /admin/tenant` | crea un tenant (`nome`, `email`, `padre` per un sotto-tenant). Risponde col **gettone di governo**, che si vede **una volta sola** |
| `GET /admin/tenant` | elenco dei tenant |
| `POST /admin/licenze` | crea una licenza: `tenant`, `piano`, e se serve `ambiti`, `quota_mensile`, `limite_al_minuto`, `giorni`, `note` |
| `GET /admin/licenze[?tenant=…]` | elenco delle licenze |
| `GET /admin/licenze/{id}` | una licenza, con le sue chiavi e il suo uso |
| `POST /admin/licenze/{id}/sospendi` | `attiva` → `sospesa` |
| `POST /admin/licenze/{id}/riattiva` | `sospesa` → `attiva`. Le **chiavi esistenti riprendono**: non vanno rifatte |
| `POST /admin/licenze/{id}/rinnova` | sposta la scadenza (`giorni`): riporta ad `attiva` una licenza **scaduta**, e **lascia sospesa una sospesa** — il rinnovo è contabilità, non è il perdono di una sospensione |
| `POST /admin/licenze/{id}/revoca` | `revocata`, e **non si torna indietro** (`stato_non_ammesso`) |
| `POST /admin/licenze/{id}/chiavi` | emette una chiave (`nome`, `ambiti`): il **valore si vede una volta sola** |
| `GET /admin/licenze/{id}/chiavi` | elenco delle chiavi (senza i valori: non ci sono più) |
| `DELETE /admin/licenze/{id}/chiavi?chiave={idchiave}` | revoca una chiave |
| `GET /admin/licenze/{id}/uso[?mese=AAAA-MM]` | uso del mese e storico |
| `GET /admin/licenze/{id}/registro` | chi ha fatto cosa, e quando. **Una riga c'è se e solo se il fatto c'è**: il cambiamento e la sua riga nascono nella stessa transazione (`D-81`, misurato il 6/10 su tutte e nove le azioni di governo), quindi non esiste un'azione avvenuta che il registro non sappia raccontare, né una riga che racconti un'azione non avvenuta |
| `GET /admin/richieste-licenza[?quanti=N]` | le richieste arrivate dal modulo della landing, **le 100 più recenti** (`quanti`, come `registro`), con `quante_mostrate` e `quante_in_tutto`: chi legge 100 deve sapere se sono 100 o 3 000 |
| `POST /admin/richieste-licenza/ritenta` | ritenta gli avvisi alla regia rimasti indietro |
| `GET /admin/motori` · `POST /admin/motori` · `DELETE /admin/motori/{nome}` | i motori. **Aggiungerne uno non cambia niente per i clienti** (kb/52 §1): finisce nel file di configurazione, non nel codice |

**Stati della licenza**: `attiva` · `sospesa` · `scaduta` · `revocata`. La **scadenza non aspetta
che qualcuno la noti**: una licenza attiva ma oltre la scadenza *è* scaduta e le sue chiavi
smettono, senza che nessuno debba toccarla.

**Da `sospesa` si esce per una porta sola: `riattiva`.** Nessun'altra azione toglie una
sospensione — un `rinnova` sposta la scadenza e la licenza resta sospesa. **Governare a caldo è
previsto**: si può sospendere o revocare una licenza *mentre lavora*, e i lavori già ammessi
finiscono e si fatturano (la quota si prenota all'ammissione), mentre la prima richiesta dopo la
sospensione prende `licenza_sospesa`. Il conto resta esatto: in fattura ci sono le pagine delle
richieste **servite**, né una in più né una in meno.

---

## 5. Console del tenant

Intestazione: `X-Gettone-Tenant: <gettone di governo>`. L'amministratore **del tenant** governa
le **sue** chiavi, dentro i limiti della sua licenza. Non può alzarsi la quota né riattivarsi
la licenza: quello è dell'amministratore Agile.

| Metodo e percorso | Cosa fa |
|---|---|
| `GET /console/licenze` | le sue licenze, il **suo uso del mese** e i suoi **sotto-tenant con quanto hanno consumato** |
| `GET /console/licenze/{id}/chiavi` | le sue chiavi |
| `POST /console/licenze/{id}/chiavi` | emette una chiave (valore una volta sola) |
| `POST /console/licenze/{id}/chiavi/{chiave}/ruota` | emette la sostituta e dà alla vecchia **24 ore di grazia** |
| `DELETE /console/licenze/{id}/chiavi/{chiave}` | revoca una chiave |
| `GET /console/licenze/{id}/uso` | il suo uso |

### Il rivenditore e i suoi sotto-tenant

Un tenant può essere il **padre** di altri tenant (`padre` alla creazione, `POST /admin/tenant`):
è il caso del rivenditore che ci porta i suoi clienti. Con una chiamata sola vede quel che consuma
lui (`uso_mio`) e quel che consuma ognuno dei suoi (`sotto_tenant[].uso`), che è quel che deve
fatturargli. Risposta `200` di `GET /console/licenze` (copiata da una chiamata vera,
`prove/p16_sotto_tenant.py`, con 2 pagine lette dal rivenditore, 3 da A e 5 da B):

```json
{
  "tenant": {"id": "tnt_7819c799bd74d450", "nome": "Rivenditore Energia s.r.l.",
             "email": null, "padre": null, "creato": 1790797352.510178},
  "uso_mio": {"mese": "2026-09", "pagine": 2, "immagini": 0, "richieste": 2,
              "quota_mensile": 20000, "residuo": 19998},
  "licenze": [{"id": "lic_c988388c73273e30", "piano": "standard", "stato_effettivo": "attiva"}],
  "sotto_tenant": [
    {"id": "tnt_f4583a45425a35cd", "nome": "Cliente A s.p.a.", "creato": 1790797352.5752263,
     "licenze": 1,
     "uso": {"mese": "2026-09", "pagine": 3, "immagini": 0, "richieste": 3,
             "quota_mensile": 20000, "residuo": 19997}},
    {"id": "tnt_0db93bea368659ce", "nome": "Cliente B s.n.c.", "creato": 1790797352.636318,
     "licenze": 1,
     "uso": {"mese": "2026-09", "pagine": 5, "immagini": 0, "richieste": 1,
             "quota_mensile": 20000, "residuo": 19995}}
  ]
}
```

(la scheda della licenza è quella di sempre, accorciata qui a tre campi.)

`uso_mio` e ogni `sotto_tenant[].uso` sono la **somma del mese corrente su tutte le licenze** di
quel tenant, con le stesse chiavi di `GET /console/licenze/{id}/uso`; chi non ha licenze o non ha
consumato porta **zeri**, non campi assenti (una riga a zero è una riga; un campo che manca è un
conto che non si fa). Gli stessi numeri li dà `GET /admin/licenze/{id}/uso` licenza per licenza.

**Un solo strato.** Un sotto-tenant non può avere sotto-tenant: `padre` che punta a un tenant che
ha già un padre è `400 campi_non_validi`, e così un anello. I clienti del cliente sono uno strato
perché un livello in più è un livello in più di fatture di cui nessuno risponde.

**Guardare non è governare.** Il padre vede l'**uso** dei figli e nient'altro: le licenze dei
figli, le loro chiavi e il loro uso licenza per licenza per lui **non esistono**
(`404 licenza_sconosciuta`), e sospendere o revocare resta dell'amministratore Agile.

**Isolamento fra tenant**: a un tenant la licenza di un altro **non è «vietata», è inesistente**
(`404 licenza_sconosciuta`). Un `403` direbbe «esiste ma non è tua», e già quello è un'informazione.

---

## 6. Modulo pubblico «richiedi una licenza»

### `POST /pubblico/richiesta-licenza`
L'unico POST aperto al mondo, quindi con tre difese: **campo trappola** `sito_web` (un umano
non lo vede e non lo compila), **limite per indirizzo IP**, e nessun dato oltre il necessario.

Il «proprio indirizzo IP» non è una cosa che si dichiara: la catena `X-Forwarded-For` conta **solo**
se chi parla al fronte è un proxy fidato (`AGILEPIXEL_PROXY_FIDATI`), e allora vale l'**ultimo**
salto — quello scritto dal proxy. Un `X-Forwarded-For` scelto dal cliente non compra un limite
nuovo (`prove/p9_proxy.py` lo misura: tre catene inventate contano per una, la terza richiesta
prende `429 troppe_richieste`).

| Campo | Obbligatorio | Caratteri al massimo |
|---|---|---|
| `azienda` | sì | 200 |
| `email` | sì | 200 |
| `referente` | no | 120 |
| `volume_mensile_stimato` | no | 60 |
| `note` | no | 2000 |
| `sito_web` | **campo trappola**: se è pieno la richiesta è rifiutata (`richiesta_rifiutata`) | — |

I cinque tetti **non sono numeri nuovi**: sono i `maxlength` che il modulo della landing dichiara
già, nelle due lingue. `maxlength` però è un attributo del browser — non viaggia nella richiesta —
e il modulo funziona anche senza JavaScript, quindi fuori da un browser li fa valere la porta:
un campo più lungo è `campi_non_validi` (400), col `dettaglio` che dice **quale** campo e
**quanto**. Un rifiuto non lascia righe e **non consuma** il limite per indirizzo
(`prove/p69_il_modulo_mantiene_la_sua_promessa.py`; il prima e il dopo in
`misure/il-modulo-e-un-secchio.tsv`).

Risposta `201`: `{"numero": "RL-20260930-a3f21c", "regia_avvisata": true}`.

Il numero è la pratica. L'avviso alla regia parte con `dico serverino` e la richiesta si segna
`inviato` **solo se `dico` esce 0**; altrimenti resta `da_avvisare` e `POST /admin/richieste-licenza/ritenta`
la ripesca. Un avviso che nessuno ha ricevuto non è un avviso.

Un'eccezione sola, e riguarda i **collaudi**: se il dominio dell'indirizzo è fra quelli che IANA
riserva alla documentazione e alle prove (RFC 2606 e RFC 6761: `.invalid`, `.test`, `.example`,
`.localhost`, `example.com|net|org`) la richiesta si registra come tutte le altre, ma con esito
`collaudo` — non arriva al centralino della regia e `ritenta` non la ripesca mai. La risposta è la
stessa, con `regia_avvisata: false`. Chi prova il modulo non sveglia un umano; un cliente sì
(`prove/p22_collaudo_non_disturba.py`).

---

## 7. ERRORI CON NOME

Ogni errore è un JSON con **`errore`** (il nome stabile, su cui si può programmare),
`spiegazione` e a volte `dettaglio`:

```json
{"errore": "quota_superata", "spiegazione": "Esaurita la quota mensile di pagine o immagini della licenza."}
```

Il nome è il contratto: lo stato HTTP può cambiare, il nome no. Un `500` **non** ha un nome di
questo elenco: se ne vedete uno, è un guasto nostro (`guasto_interno`) e sta nei nostri registri.

Anche una richiesta **malfatta** (campo obbligatorio mancante, file mancante, JSON storto) torna un
nome di questo elenco — `campi_non_validi`, con in `dettaglio` i **campi** che non tornano, mai i
valori — e non la forma di errore del nostro framework: su un contratto si programma, e il caso più
facile da sbagliare non può essere l'unico fuori contratto.

Anche **`quota_superata`** porta un `dettaglio`: il mese, il consumo del mese e la quota del piano
(per esempio `mese 2026-10: 500 su 500 fra pagine e immagini, questa richiesta ne chiedeva 50`).
Chi prende il 429 deve poter decidere **senza una seconda chiamata** se aspettare il mese nuovo o
chiedere un piano più grande. Il nome resta il contratto: il `dettaglio` è testo per gli umani,
non un campo su cui programmare.

### Quando riprovare: `Retry-After`

I due errori di limite che conoscono l'**ora** in cui la porta si riapre la dicono, in
un'intestazione HTTP standard (`Retry-After`, RFC 9110 §10.2.3) e in **secondi**:

| Quando | Perché il numero è esatto |
|---|---|
| `429 troppe_richieste_al_minuto` | il limite al minuto conta dentro un minuto intero di orologio: la finestra in corso si riapre a un'ora nota. Il `dettaglio` dice anche **qual è** il limite della licenza, così chi lo prende sa se cambiare passo o cambiare piano |
| `429 troppe_richieste` (sportello pubblico §6) | la finestra per indirizzo **scorre**: il posto si libera quando ne esce il colpo più vecchio, che è un'ora nota anche lei |

Il numero è il primo istante in cui riprovare **funziona**: arrivarci un secondo prima dà un
altro 429, ed è misurato nei due versi (`prove/p94_quando_riprovare_e_un_numero.py`). Una
libreria HTTP che ritenta da sé lo legge senza sapere niente di noi.

**Dove l'intestazione non c'è, vuol dire che nessuno sa quando.** Non esce su `quota_superata`
(là la risposta non è aspettare: è il mese nuovo o un piano più grande, e il `dettaglio` dà i
numeri per scegliere), non esce sui guasti del motore (`motore_in_guasto`,
`nessun_motore_disponibile`) e non esce su `coda_piena`. Sono i casi più ritentabili di tutti, e
per questo i più pericolosi da numerare: un'attesa inventata manderebbe **tutti** i client
rimbalzati a ribussare nello stesso istante. L'assenza è un'informazione, non una dimenticanza.

E **`motore_in_guasto`** porta un `dettaglio` quando il motore ha **rifiutato** la richiesta
(`4xx`): quel che il motore ha scritto sul perché, per esempio `questo modello è di VISTA e
guarda solo image/png o image/jpeg`. Senza quella riga il cliente leggeva «il motore ha risposto
con un errore» e andava a cercare un guasto che non c'è, mentre l'unica cosa che poteva cambiare
era la sua richiesta. Tre cose da sapere: il perché arriva **solo** dai rifiuti, non dai guasti
veri (`5xx`, connessione, tempo scaduto), perché là il fronte **ricade** su un altro motore e il
perché dell'ultimo tentativo di tre racconterebbe una cosa parziale; è **tagliato** a 300
caratteri su una riga sola, con i puntini a dire che è tagliato, perché lo scrive un processo che
non è nostro; e **non riporta mai** il contenuto del documento, nemmeno un pezzo. Come tutti i
`dettaglio`, è testo per gli umani: su `errore` si programma, su questo no.

Lo stesso `dettaglio` dice **un'altra** cosa in un caso solo, e non viene dal motore ma da noi:
quando il motore ha risposto `200` **senza il carico utile** (§3, *La ricaduta*), il `dettaglio`
nomina le
chiavi che mancavano. È l'unico caso in cui `motore_in_guasto` parla dopo un `200`.

| Nome | HTTP | Significato |
|---|---|---|
| `campi_non_validi` | 400 | I dati della richiesta non sono validi. |
| `documento_mancante` | 400 | Serve un documento (campo «documento» in multipart/form-data). |
| `piano_sconosciuto` | 400 | Piano non previsto: i piani sono prova, standard, enterprise. |
| `richiesta_rifiutata` | 400 | Richiesta rifiutata dai controlli di ammissione. |
| `schema_non_valido` | 400 | Lo schema JSON dei campi da estrarre non è valido. |
| `chiave_mancante` | 401 | Serve una chiave API nell'intestazione Authorization: Bearer <chiave>. |
| `chiave_revocata` | 401 | La chiave è stata revocata. |
| `chiave_scaduta` | 401 | La chiave è scaduta (una chiave ruotata resta valida 24 ore, poi smette). |
| `chiave_sconosciuta` | 401 | La chiave non esiste. |
| `ambito_mancante` | 403 | L'ambito chiesto non è fra quelli disponibili: la chiamata lo richiede e la chiave non l'ha, o si chiede per una licenza o una chiave un ambito che non esiste o che il livello sopra non ha (leggi, estrai, descrivi). |
| `licenza_revocata` | 403 | La licenza è stata revocata e non si riattiva. |
| `licenza_scaduta` | 403 | La licenza è scaduta: va rinnovata. |
| `licenza_sospesa` | 403 | La licenza sotto cui sta la chiave è sospesa: riattivarla per riprendere. |
| `non_autorizzato` | 403 | La chiave esiste ma non ha il diritto di fare questa operazione. |
| `licenza_sconosciuta` | 404 | Nessuna licenza con questo identificativo (o non è del tenant che chiede). |
| `rotta_sconosciuta` | 404 | La rotta chiesta non esiste: le rotte di AgilePixel sono in API.md e in /openapi.json. |
| `tenant_sconosciuto` | 404 | Nessun tenant con questo identificativo. |
| `stato_non_ammesso` | 409 | Il passaggio di stato chiesto non è ammesso da quello attuale. |
| `documento_troppo_grande` | 413 | Il documento supera la dimensione massima ammessa. |
| `troppe_pagine` | 413 | Il documento ha più pagine del massimo ammesso per una singola richiesta. |
| `formato_non_supportato` | 415 | Formato non supportato: si accettano PDF, PNG, JPEG, TIFF, WEBP. |
| `documento_protetto` | 415 | Il documento è protetto da una password: AgilePixel non la chiede e non la conserva, va spedita una copia senza password. |
| `quota_superata` | 429 | Esaurita la quota mensile di pagine o immagini della licenza. |
| `troppe_richieste` | 429 | Troppe richieste da questo indirizzo: riprovare più tardi. |
| `troppe_richieste_al_minuto` | 429 | Superato il limite di richieste al minuto della licenza. |
| `lavoro_annullato` | 499 | Il client ha chiuso la connessione: il lavoro è stato annullato e non consuma quota. |
| `motore_in_guasto` | 502 | Il motore ha risposto con un errore: la richiesta non è stata servita. |
| `coda_piena` | 503 | La coda del servizio è piena: riprovare fra poco. |
| `motore_fuori_dal_giro` | 503 | I motori che sanno fare questo lavoro ci sono ma in questo momento sono fuori dal giro: il servizio li ricontrolla e la richiesta si può ripetere. |
| `nessun_motore_disponibile` | 503 | Nessun motore è registrato, o nessuno di quelli registrati ha mai preso un lavoro: il servizio sa smistare ma non c'è dove smistare. |
| `motore_senza_modello` | 503 | Questo documento ha bisogno di un motore con modello (una scansione, una foto, un PDF di sola immagine) e in questo momento non ce n'è uno pronto: i PDF che portano dentro il loro testo si leggono comunque. |
| `motore_non_risponde` | 504 | Il motore non ha risposto entro il tempo massimo. |

*Questo elenco è generato dal codice e verificato: la prova `prove/p5_contratto.py` confronta i
nomi di `servizio/errori.py` con quelli di questa tabella e **fallisce se i due insiemi non
coincidono**. Non si può aggiungere un errore senza documentarlo, né documentarne uno che non c'è.*

**Chi sbaglia l'indirizzo.** `rotta_sconosciuta` (§1) lo alzano **due** posti, e dicono la stessa
cosa: il **proxy**, quando sbagli il **nome di base** — `/admin/licenze` sul nome della landing,
dove le API non ci sono — e il **servizio**, quando sbagli la **rotta**: `/v1/documenti/estri`
invece di `estrai`. Il secondo è il caso **normale** e fino all'1/10/2026 era l'unico senza nome
(rispondeva `{"detail": "Not Found"}`, il corpo di FastAPI): oggi è nella tabella qui sopra come
tutti gli altri, perché lo alza il catalogo del servizio. Una rotta che non esiste **non esiste
per nessuno**: risponde `404` prima di guardare chi chiede (livello 1 della precedenza qui sotto),
e la risposta è la stessa senza chiave, con una chiave finta e col gettone d'amministratore giusto —
se fosse diversa, direbbe a un estraneo **quali** prefissi sono guardati.

**`nome_sconosciuto`, l'unico nome della porta d'ingresso fuori tabella.** La tabella è **generata dal catalogo del
servizio**, e quel nome lo alza **solo** il proxy — un nome che non è nessuno dei due non arriva
mai al servizio, che quindi non può averlo in catalogo. Ha la **stessa forma** di tutti gli altri —
`{"errore": …, "spiegazione": …}` con `Content-Type: application/json` — perché su quello si
programma come su tutti gli altri (`prove/p9_proxy.py` lo misura dal vivo, sul `Caddyfile` del
repo; `prove/p41_il_404_ha_un_nome.py` misura il 404 del servizio).

**Il tetto del corpo, e chi lo alza.** `documento_troppo_grande` (413) lo alzano **due** posti,
come `rotta_sconosciuta`: il **servizio**, che misura il **documento** contro i 40 MiB di §8, e la
**porta d'ingresso**, che misura **tutto il corpo HTTP** contro un tetto appena più alto — 41 MB,
cioè i 40 MiB del documento più l'involucro `multipart` (166 byte misurati). Il nome, lo stato e
la forma del corpo sono gli stessi da tutti e due: su un nome si programma, e il cliente non deve
sapere quale dei due tetti ha toccato. La differenza che si vede è un'altra, e è a suo favore: il
tetto della porta d'ingresso guarda `Content-Length` e risponde **prima di leggere il corpo** —
misurato il 5/10/2026 dal posto del cliente, chi manda 50 MB ne carica 1,9 e riceve il nome in
501 ms, invece di caricarne 50 per poi sentirsi chiudere la porta in faccia. Fino al 5/10/2026 lì
non c'era nessun nome: il tetto della porta stava a 48 MB mentre il servizio ammetteva 40 MiB, e
sopra i 48 la risposta era un **`502` con il corpo vuoto** (misurato: `Content-Length: 0`,
`Server: Caddy`, dopo aver accettato tutti i 52 428 966 byte) — l'unico cliente senza niente su
cui programmare era quello che aveva mandato il documento più grosso. Provato in
`prove/p66_il_tetto_del_corpo.py`, controprova compresa.

**L'eccezione che resta, e si vede: il metodo sbagliato.** Un metodo che una rotta esistente non
ammette risponde `405` con `{"detail": "Method Not Allowed"}` — **senza** nome — e con la testata
`Allow` che dice i metodi ammessi (quella, dall'1/10/2026, è l'unione di **tutte** le rotte che
combaciano con l'indirizzo: prima era quella della prima e sbagliava su 7 porte su 22). È l'unico
errore del servizio senza un nome del catalogo: dargliene uno è un nome **nuovo**, cioè un
contratto **1.1**, e la decisione è della regia (biglietto 31). Finché è così, è scritto qui.

### Precedenza fra nomi

Quando una richiesta sbaglia in più modi insieme, **un solo nome** esce. Quale, lo dice il
contratto — perché su un nome si programma, e indovinare la porta non è programmare. Questo è
l'ordine che il servizio **applica davvero**: non è un desiderio, è misurato coppia per coppia in
`prove/p34_un_nome_solo.py` (punto 8), e se qualcuno sposta un controllo la prova cade.

0. **l'indirizzo** — `rotta_sconosciuta`. Prima di tutto il resto: se la rotta non esiste, non
   esiste **per nessuno**, e il servizio non guarda nemmeno il corpo né la chiave. Misurato
   (`prove/p41_il_404_ha_un_nome.py`): `POST /v1/non-esiste` **senza corpo e senza chiave** è
   `404 rotta_sconosciuta`, non `campi_non_validi` (livello 1) e non `chiave_mancante`
   (livello 2), mentre lo **stesso** corpo mancante su una rotta che **esiste** è
   `400 campi_non_validi`; `GET /admin/non-esiste` dà la **stessa** risposta senza gettone, con
   un gettone sbagliato e col gettone giusto. Questo livello non è una scelta nostra ed è il
   motivo per cui sta prima: lo decide chi smista — il proxy per il **nome di base**, il router
   del servizio per la **rotta** — e un 404 che cambiasse con la chiave direbbe a un estraneo
   quali prefissi sono guardati. Il metodo sbagliato su una rotta che esiste è un'altra cosa
   (`405`, vedi qui sopra), e non è in questo elenco perché non ha un nome.

1. **la forma della chiamata** — `campi_non_validi`. Una richiesta che non è nemmeno una chiamata
   (manca il campo `documento`) non arriva alla guardia: risponde `400` col nome del campo in
   `dettaglio`, **anche senza chiave**. L'unica cosa che rivela è il nome di un campo che questa
   pagina pubblica. Attenzione: un campo `documento` **presente e vuoto** è un'altra cosa, ed è
   `documento_mancante` (vedi il livello 6);
2. **chi sei** — `chiave_mancante`, `chiave_sconosciuta`, `chiave_revocata`, `chiave_scaduta`, e
   sulle porte di governo `non_autorizzato`: a uno sconosciuto non si raccontano i fatti nostri;
3. **cosa esiste per te** — `tenant_sconosciuto`, `licenza_sconosciuta`. La licenza di un altro
   tenant **non esiste**, non è «vietata»: vale anche quando la richiesta è sbagliata anche in
   altro modo, perché chi non ha la licenza non deve nemmeno sapere quali ambiti ha;
4. **lo stato della licenza** — `licenza_sospesa`, `licenza_scaduta`, `licenza_revocata`;
5. **cosa chiedi** — e qui la **forma viene prima del merito**: sulle porte di governo un campo
   malfatto è `campi_non_validi` (col campo in `dettaglio`), un piano che non c'è è
   `piano_sconosciuto`, un passaggio di stato impossibile è `stato_non_ammesso`, e **solo dopo**
   arriva il merito, `ambito_mancante`. Un `ambiti` che non è una lista di nomi non è un ambito
   che manca: è un campo malfatto. Poi i parametri della chiamata: `schema_non_valido`,
   `richiesta_rifiutata`;
6. **il documento** — `documento_mancante`, `formato_non_supportato`, `documento_protetto`,
   `troppe_pagine`, `documento_troppo_grande`. Quest'ultimo è l'unico di questo livello che può arrivare **prima**
   di tutti gli altri, e non per una scelta di merito: il tetto del corpo lo guarda la **porta
   d'ingresso** su `Content-Length`, cioè prima che esista una chiave da controllare (livello 2) o
   un corpo da leggere. Chi sfonda il tetto del corpo riceve quindi `documento_troppo_grande` anche
   **senza chiave** e anche su una rotta che non esiste. Non è una contraddizione col livello 0: è
   lo stesso motivo per cui il livello 0 sta prima di tutto — lo decide chi smista, non chi
   autorizza — e la cosa che rivela è un numero che questa pagina pubblica;
7. **quanto puoi fare** — prima `quota_superata`, poi `troppe_richieste_al_minuto`.
   `troppe_richieste` è il freno **per indirizzo IP** e vale solo sullo sportello pubblico §6;
8. **chi serve la richiesta** — `coda_piena`, `nessun_motore_disponibile`,
   `motore_fuori_dal_giro`, `motore_in_guasto`, `motore_non_risponde`, `lavoro_annullato`.
   **I due 503 dei motori non dicono la stessa cosa, e si trattano in due modi.**
   `nessun_motore_disponibile` vuol dire **non c'è dove smistare**: nessun motore è
   registrato, o nessuno di quelli registrati ha mai preso un lavoro — è la verità di oggi,
   perché il motore GPU non esiste ancora (§3), e riprovare fra un secondo non cambia
   niente. `motore_fuori_dal_giro` vuol dire il contrario: il motore **c'è** e ha lavorato
   per noi, ed è uscito dal giro per poco — tipicamente perché ha risposto con un errore a
   **una** richiesta (di un altro cliente, non necessariamente tua) e il fronte lo
   ricontrolla al battito successivo (§3, «ogni pochi secondi»; il numero di oggi arriva nel
   `dettaglio`). Questa è la risposta da **ripetere**. Fino al 4/10/2026 il secondo caso
   portava il nome del primo, e il nome era falso: misurato, un motore che aveva rifiutato
   una pagina diceva a tutti gli altri clienti «non c'è nessun motore» mentre stava
   benissimo e rispondeva 200 al colpo dopo.

**Il documento viene prima dei limiti (6 prima di 7), e questo si paga.** Misurato: con il limite
al minuto già sfondato, un documento di formato sbagliato prende `formato_non_supportato` e **non**
`troppe_richieste_al_minuto`; con la quota esaurita, un PDF di 60 pagine prende `troppe_pagine` e
non `quota_superata`. Per il cliente è la risposta più utile — gli si dice che cosa c'è di storto
nel documento — ma vuol dire anche che **un documento storto non consuma né quota né limite al
minuto**, e aprirlo costa a noi (un PDF da 30 MB con la tabella degli oggetti guasta costa 2,5 s di
CPU, misurato in `prove/p25_ammissione_non_ferma_la_casa.py`). È un punto **aperto** del contratto,
non un caso: spostare il conteggio del limite al minuto prima dell'ammissione cambierebbe il nome
che il cliente vede, e il nome è il contratto.

**Un ambito che non si può avere ha un nome solo, da tutte le porte: `ambito_mancante` (403).**
Vale per `POST /admin/licenze`, per le due porte che emettono una chiave
(`POST /admin/licenze/{id}/chiavi` e `POST /console/licenze/{id}/chiavi`) e per le tre porte di
prodotto. Il `dettaglio` elenca gli ambiti **disponibili** a quel livello, e non ripete quelli
chiesti: un messaggio d'errore non è l'eco di quel che avete mandato. Fino all'1/10/2026 lo stesso
sbaglio usciva con **tre nomi** secondo la porta — `prove/p34_un_nome_solo.py`.

**Questo elenco contiene tutti i nomi del catalogo.** Un errore nuovo non entra nel contratto senza
prendere il suo posto nell'ordine: la prova lo esige.

---

## 8. Limiti e regole

| Limite | Valore | Perché |
|---|---|---|
| Pagine per richiesta | **50** | meglio un `troppe_pagine` in un millisecondo che una GPU occupata dieci minuti da un PDF di mille pagine (lezione di AgileV, kb/24) |
| Dimensione del documento | **40 MiB** (41 943 040 byte) | `documento_troppo_grande`, dal servizio |
| Dimensione del **corpo HTTP** della richiesta | **41 MB** (42 991 616 byte) | lo stesso `documento_troppo_grande`, dalla porta d'ingresso e **senza leggere il corpo**: il corpo HTTP è il documento più l'involucro `multipart` |
| Lavori in volo · coda | 4 · 32 **di tutto il servizio**, non per licenza | oltre: `coda_piena` (503), subito e con un nome |
| Tempo massimo di una richiesta al motore | **120 s** | è il tempo **della richiesta**, ricadute comprese: non 120 s per ogni motore provato |
| Formati | PDF, PNG, JPEG, TIFF, WEBP | riconosciuti dal **contenuto** |
| Una scansione **TIFF di più facciate** | conta e si paga **una pagina per facciata** | è il formato in cui uno scanner o un multifunzione impacchetta più fogli in un file: le facciate si contano all'ingresso, il massimo di pagine vale su quel numero, e il modello le guarda **tutte**. Negli altri formati più di un fotogramma è lo stesso scatto in un'altra versione (un MPO di telefono, un'animazione) e vale **una** pagina |
| Un PDF **cifrato** | si legge se la **password utente è vuota**, qualunque sia il cifrario (RC4, AES-128, AES-256) | è il PDF che si apre in qualunque lettore senza chiedere niente: la cifratura di chi emette serve a impedire le **modifiche**. Le facciate si contano e si pagano come in chiaro. Un documento chiuso da una password **vera** prende `documento_protetto` (415): la password non la chiediamo e non la conserviamo, va spedita una copia aperta |
| Pagine per documento | **almeno 1** | un documento con zero pagine non è un documento: `formato_non_supportato`. Un PDF valido e vuoto esiste, e senza questa riga veniva ammesso, spedito al motore e fatturato **zero** (misurato, `prove/p26_documento_sporco.py`) |

**I posti sono del servizio, non del tuo piano.** `4 · 32` è la capacità di tutta la casa e
si **divide con tutti gli altri clienti**: la coda non sa di chi è il lavoro. Quota mensile e limite
al minuto (§2) sono **tetti su di te** — dicono quanto puoi chiedere, non quanti posti ti spettano —
perciò **`coda_piena` può arrivare per il traffico di un altro cliente**, con la tua quota intera e
alla tua prima richiesta del minuto: la causa non è nel tuo traffico e guardando solo il tuo non la
troveresti. Misurato dal vivo il 3/10/2026 con due tenant veri (`max_in_volo=1`, `max_in_attesa=4`,
`prove/p58_i_posti_sono_del_servizio.py`): il primo manda 5 richieste e riempie; il secondo, altra
licenza e altra chiave, prende **`503 coda_piena` alla sua prima richiesta in 55 ms** e **non
consuma quota** (la prenotazione torna indietro: 0 pagine prima, 0 dopo), e la porta gli resta
chiusa **18,6 s**, il tempo dei lavori che ha davanti. Non serve un abuso: coi numeri di casa
bastano **36 richieste insieme** a un tenant per occupare tutto, e 36 sta **dentro** il 60 al minuto
del piano `standard`. Che fare: `coda_piena` è un 503 e si **ritenta**, con attesa che raddoppia;
`/health` dice la profondità della coda nel momento. *Quanta coda possa tenere un singolo tenant è
una decisione aperta (D-58): oggi il contratto non contiene quel numero, e quando ci sarà questa
riga cambierà.*

**Il tetto del corpo si paga una volta, alla porta.** I due numeri qui sopra sono **due tetti sullo
stesso documento**, e si danno il cambio sul medesimo nome: fino a 40 MiB il documento passa e viene
servito; fra 40 MiB e 41 MB arriva al servizio e prende `documento_troppo_grande` da lui; oltre i
41 MB lo ferma la porta d'ingresso, con lo stesso nome, lo stesso `413` e lo stesso corpo JSON, dopo
aver letto **zero** byte del documento. Misurato dal vivo il 5/10/2026, dal posto del cliente
(`prove/p66_il_tetto_del_corpo.py`): 40 MiB esatti → servito, tutti i byte accettati; 40 MiB +
56 960 byte → `413 documento_troppo_grande` dal servizio; 50 MB → `413 documento_troppo_grande`
dalla porta, **1 900 544 byte caricati su 52 428 966** e risposta in 501 ms. Vale anche per un corpo
mandato a pezzi (`Transfer-Encoding: chunked`), che non dichiara una lunghezza: lì il tetto scatta
mentre legge, e il nome è lo stesso. *Fino al 5/10/2026 il tetto della porta stava a 48 MB e sopra
quel numero rispondeva un `502` con il corpo vuoto, senza nome: era un guasto, non una scelta.
**La cura è nel repo (`proxy/Caddyfile`) e in esercizio NON c'è** finché la regia non rilascia — al
box il rilascio è negato — perciò oggi un cliente in produzione, sopra i 48 MB, vede ancora il `502`
muto.*

**Il client che chiude uccide il lavoro** entro un battito, e quel lavoro **non consuma quota**
(la «coda fantasma» del 12/9, kb/24 §7). Un lavoro che nessuno aspetta più non merita una GPU.

**La quota si PRENOTA all'ammissione, non si conta alla fine.** Le pagine di una richiesta
entrano nel conteggio d'uso *prima* che il motore lavori, e tornano indietro se il lavoro non
arriva in fondo (coda piena, motore caduto, client che ha chiuso, **o un `200` del motore senza
il carico utile** — §3, *La ricaduta*). Due conseguenze per chi ci
scrive contro: il tetto mensile tiene **esatto** anche con mille richieste insieme — la richiesta
che lo supera prende `quota_superata` e non consuma niente — e un conteggio d'uso letto **a metà
di una raffica** può comprendere pagine ancora in volo, che scenderanno se quei lavori cadono.
I numeri di fine mese sono quelli dei lavori finiti.

**E tornano indietro anche se il servizio si riavvia.** Un fronte ucciso **con un lavoro in volo**
— uno schianto, un OOM, uno spegnimento che sfonda il tetto di `systemd` — non lascia quelle
pagine nel tuo conto: la prenotazione è scritta nel deposito, e all'avvio il servizio restituisce
quelle dei lavori di un processo che non c'è più. Uno spegnimento **ordinato**
(`systemctl restart`) porta invece il lavoro in volo fino in fondo: lo ricevi, e paghi quel che
hai ricevuto. Provato dal vivo in `prove/p63_la_prenotazione_non_resta.py` — compreso il confine
che conta: due lavori uccisi su una licenza da quota 10 **non** la chiudono per il mese.

**Privacy (GDPR — le bollette sono dati personali).** Nei log non entra **nessun contenuto**:
solo l'impronta del documento, il numero di pagine, i millisecondi, il nome del motore e
l'identificativo della licenza. Nessun valore di chiave, nessun gettone. I documenti non si
scrivono su disco. E non è solo il contenuto: **nessun testo libero che mandi** (il nome del file,
il `tipo`, i nomi dei campi del tuo `schema`, il nome di una chiave o di un tenant, i campi del
modulo pubblico) finisce in un log.

Chi lo verifica, e come, perché una promessa di privacy senza una misura è una frase: due prove
**dal vivo**, non un `grep` del banco. `prove/p83` manda cinque canarini in una bolletta e uno nel
nome del file; `prove/p84` ne manda **uno diverso su ogni porta** — le 38 rotte del fronte sono
classificate una per una, 11 accettano testo libero di un cliente — e boccia se un canarino
compare in uno dei tre log (fronte, adattatore, motore) o in una colonna del deposito che non è la
sua. Il 6/10/2026 hanno trovato e fatto curare tre cose: il JSON malformato di un modello nel log
del motore, l'indirizzo e-mail di chi compila il modulo pubblico nel log del fronte, e la
**query** nella riga d'accesso.

Della riga d'accesso, per chi deve scrivere un DPA: ci sono il **metodo**, il **percorso**,
l'esito e l'**indirizzo IP** di chi chiama; la **query NON c'è** — al suo posto c'è `?…` —, quindi
quel che metti in `?mese=`, `?tenant=` o `?chiave=` non viene scritto. Il percorso invece **c'è**:
i suoi segmenti sono identificativi e verbi di un insieme chiuso, e non ci va messo altro.
L'indirizzo IP resta (serve a chi tiene su il servizio, e al limite per indirizzo del modulo
pubblico): per quello la risposta è la **conservazione**, non la cancellatura.

**Ripiego su fornitori terzi**: **disattivato per default** per i documenti con dati personali.
Un eventuale ripiego è solo UE e solo per i clienti che lo accettano per contratto.

---

## Versioni del contratto

| Versione | Data | Cosa cambia |
|---|---|---|
| **1.0** | 30/9/2026 | prima versione: accesso, lettura, estrazione, descrizione, contratto dell'amministratore, console del tenant, modulo pubblico. Il motore non c'è ancora: `/health` lo dice. |
| **1.1** | 6-7/10/2026 | **aggiunti** i nomi di motivo `fogli_in_disaccordo` (il terzo filtro, §2) e `tipo_non_promesso` (il quarto, §2) in `motivi`: **nessuna chiave nuova**, nessun percorso nuovo, nessun errore nuovo — `motivi` è per contratto una lista di nomi che cresce, e chi la legge per nome non cambia nulla. **aggiunti** `GET /salute` e `GET /salute/pronto`, nomi italiani della salute del servizio (i servizi sovrani di Agile si chiamano in italiano). **NON è stato tolto niente**: `/health` e `/health/pronto` restano, con lo stesso corpo e senza scadenza; nessun percorso del `1.0` sparisce, nessun parametro facoltativo diventa obbligatorio, nessun codice di risposta scompare, nessuno dei trenta nomi d'errore se ne va. `contratto/openapi-1.0.json` **resta nel repo** e `prove/p89` lo usa come metro: se una di quelle quattro cose cadesse, questa non sarebbe una `1.1` ma una `2.0`. |

Un cambio **che rompe** un client fa nascere una versione nuova (`contratto/openapi-2.0.json`) e
la vecchia resta servita. Un'aggiunta che non rompe niente resta nella 1.0.

**Aggiunte restate nella 1.0** (nessun percorso nuovo, nessun campo obbligatorio nuovo, nessun
nome d'errore che sparisce; dove il documento OpenAPI non cambia di un byte lo dice la riga):

| Data | Aggiunta | Perché non rompe |
|---|---|---|
| 4/10/2026 | il nome d'errore `motore_fuori_dal_giro` (503, §7 e §8) accanto a `nessun_motore_disponibile`, che resta e resta vero | è un 503 come prima, al posto di un 503 che in quel caso **mentiva**: un client che riprova sui 503 non cambia una riga, e uno che guarda i nomi ne trova uno in più e nessuno in meno |
| 5/10/2026 | il tetto del **corpo HTTP** (41 MB, §7 e §8) risponde `documento_troppo_grande` dalla porta d'ingresso, dove prima rispondeva un `502` muto | nessun nome nuovo: `documento_troppo_grande` era già in §7 al livello 6, e lo alzava già il servizio per lo stesso sbaglio. Il tetto **scende** da 48 MB a 41 MB, e un client che ci stava dentro (il contratto ne promette 40 MiB) non vede nessuna differenza; chi ci stava sopra riceveva un corpo vuoto e ora riceve un nome |
| 5/10/2026 | `GET /admin/richieste-licenza` prende `quanti` (default **100**, il numero della rotta sorella `registro`) e la risposta aggiunge `quante_mostrate` e `quante_in_tutto` | il percorso c'era, il parametro è **facoltativo** e i due campi sono **in più**: nessun campo sparisce. Qui il documento OpenAPI **cambia** (il parametro nuovo), e per questo `contratto/openapi-1.0.json` è rigenerato con `contratto/versiona.py`: un parametro facoltativo non rompe nessun client (D-61, la politica è «un'aggiunta che non rompe niente resta nella 1.0»). Quel che cambia davvero è il **default**: chi chiamava senza parametro riceveva TUTTE le richieste e ora ne riceve le 100 più recenti, con `quante_in_tutto` che dice quante sono — ed è il motivo del biglietto 64, perché senza tetto quella risposta cresceva per sempre |
| 6/10/2026 | l'intestazione **`Retry-After`** sui due 429 che sanno l'ora (`troppe_richieste_al_minuto`, `troppe_richieste`), e un `dettaglio` sul primo dei due (§7) | nessun percorso nuovo, nessun nome d'errore nuovo, nessuno stato HTTP che cambia: è un'**intestazione in più** su risposte che c'erano già, e un client che non la guarda si comporta esattamente come ieri. Il documento OpenAPI non cambia di un byte. Dove il numero non è esatto l'intestazione **non esce**, che è la ragione per cui non è una promessa nuova ma la stessa risposta detta meglio (`prove/p94_quando_riprovare_e_un_numero.py`) |
| 7/10/2026 | **un `200` del motore senza il carico utile non si consegna e non si fattura** (§3, *La ricaduta*; il `dettaglio` di `motore_in_guasto` in §7) | nessun percorso nuovo, nessun nome d'errore nuovo, nessuna chiave nuova nelle risposte `200`: cambia **quali risposte escono**, e quelle che prima uscivano erano `200` **senza** quel che il cliente aveva chiesto, pagati (3 pagine su 3, `misure/un-200-del-motore-non-e-una-risposta.tsv`). Un client che riceveva quei `200` doveva già gestire un `502 motore_in_guasto`, che il contratto ha da sempre. Il documento OpenAPI non cambia di un byte (`prove/p96` e `prove/p89`) |
