AgileDecide guida all'API

AgileDecide — guida all'API /v1/systemone

Per chi deve chiamare il servizio da un proprio programma. Scritta il 26/10/2026.

Derivata da tre fonti, non da ricordi: il contratto docs/F2-contratto.md, il codice della porta servizio/porta.py, l'elenco dei compiti servizio/registro.json. Gli esempi curl ricalcano le chiamate di servizio/collaudo.py. Dove una cosa non è misurata, è scritto.


1. Cos'è, e da dove si chiama

AgileDecide è un servizio di decisioni a risposta fissa. Gli dai un testo e il nome di un compito; lui sceglie una classe da un elenco chiuso, dice quanto è sicuro, e si astiene quando non lo è.

È una normale porta HTTP. Richieste e risposte sono JSON (application/json, UTF-8). Ogni chiamata porta una chiave nell'intestazione Authorization: Bearer <chiave>.

Il nome pubblico c'è. La base delle chiamate è https://api.agiledecide.agile.software. Davanti al servizio sta un proxy nginx 1.24 della regia, che inoltra al servizio solo il percorso /v1/: ogni altra rotta prende 404 percorso_ignoto dal proxy, e la richiesta non arriva nemmeno a noi. La vetrina statica sta su un nome diverso, https://agiledecide.agile.software. La superficie pubblica è soltanto 80 e 443: tutto il resto è chiuso. Configurazione e certificati sono della regia (infra/agiledecide/nginx-agiledecide.conf, sul serverino), non di questo box: questa guida non promette di gestirli.

Il servizio dietro il proxy non si è aperto di un millimetro, e conviene saperlo perché spiega gli esiti che vedrete:

La porta TCP di esercizio è la 8711 (servizio/systemone.service), e si raggiunge solo dalla macchina stessa. Negli esempi della §7 la base è quella pubblica, https://api.agiledecide.agile.software: è l'unica che un cliente possa chiamare, e i comandi si copiano così come sono. L'indirizzo di casa http://127.0.0.1:8711 resta dichiarato a parte, in una seconda variabile, perché /healthz non esce dalla macchina e perché è lì che il nostro collaudo esegue gli esempi (strumenti/collaudo_cliente.py, che sostituisce ogni variabile dichiarata con un'istanza usa-e-getta). Fra le due basi il resto del comando non cambia.

Le rotte sono tre, e sono tutte quelle che esistono (ROTTE in servizio/porta.py):

rottametodochiavea cosa serve
/v1/compitiGETsìl'elenco dei compiti e cosa il servizio sa fare di ciascuno
/v1/systemonePOSTsìuna decisione su un testo
/healthzGETnostato del processo, solo dalla macchina stessa

/healthz non esce dalla macchina nemmeno verso l'altro chiamante ammesso: da fuori risponde 404, non 403, così non risulta nemmeno che quel percorso esista. Non usatelo come controllo di salute remoto: non lo potete raggiungere. Dal nome pubblico /healthz non esiste affatto: lo ferma il proxy, che inoltra solo /v1/, e la risposta è il suo 404. Le rotte raggiungibili da fuori sono dunque due, /v1/compiti e /v1/systemone.


2. GET /v1/compiti — che cosa il servizio sa fare

Torna l'elenco completo dei compiti, compresi quelli che oggi il servizio non serve — che un compito non sia servito è un'informazione utile, non una cosa da nascondere — e l'ambito della vostra chiave, cioè su quali compiti potete chiamare. Ogni voce è la voce del registro servizio/registro.json con id rinominato in compito:

camposignificato
compitot1, t2, t3, t4
nomela descrizione in italiano del compito
versione_schema, impronta_schemaversione dello schema servito (es. "2.3") e sua impronta, per accorgersi che è cambiato
classil'elenco chiuso delle classi possibili
lunghezza_max_caratterioltre questa lunghezza il servizio si astiene
lingue, campi_obbligatorile lingue previste e i campi che la richiesta deve portare
livello, soglia, confinequale motore decide e con quale soglia; null se nessuno. confine è una soglia più alta che vale solo su alcune classi: su quelle il taglio è la più alta fra soglia e confine
servitose oggi il servizio decide davvero su questo compito
perchein prosa, perché è servito o perché no
a_valle_astensioneche cosa deve fare il vostro sistema quando arriva ASTIENI
asimmetriaquale errore costa di più su questo compito
configurazioneche cosa abbiamo misurato di meglio su questo compito, in parole libere (vocabolario aperto): può nominare anche una configurazione che NON è servibile — per esempio ospitata — perché una misura migliore deve avere un posto dove arrivare; null se non c'è
vincolii vincoli dello schema, con l'esito misurato di ciascuno

Risposta (estratto reale dalla voce t2 di servizio/registro.json; i campi lunghi in prosa ci sono davvero, qui sono accorciati a …):

{
  "versione_risposta": "1",
  "ambito": ["t1", "t2", "t3", "t4"],
  "compiti": [
    { "compito": "t2",
      "nome": "Instradamento verso le specialità di AgileLLM",
      "versione_schema": "2.3", "impronta_schema": "6099722f678dfec2",
      "classi": ["NORMATIVA", "AIUTO_PRODOTTO", "DI_SERIE"],
      "lunghezza_max_caratteri": 2000, "lingue": ["it", "en"],
      "campi_obbligatori": ["testo"],
      "livello": null, "soglia": null, "confine": null, "servito": false,
      "perche": "…", "a_valle_astensione": "…", "asimmetria": "…",
      "configurazione": "misurata meglio: nessuna configurazione sola tiene i tre vincoli",
      "vincoli": ["…"] }
  ]
}

3. POST /v1/systemone — una decisione

La richiesta

Content-Type: application/json. Il corpo è un oggetto con questi campi e nessun altro: un campo non previsto è un errore, non viene ignorato.

campotipoobbligatorionote
compitostringasìuno fra t1…t4; deve esistere nel registro
testostringasìil testo da classificare; può essere vuoto, ma allora il servizio si astiene
contestooggetto di stringhedipendei campi obbligatori li dice lo schema del compito (campi_obbligatori in /v1/compiti); chiavi e valori devono essere stringhe
id_chiamantestringanofinisce nei registri del servizio: forma chiusa [A-Za-z0-9._:-], massimo 64 caratteri

Limiti duri, dal codice (servizio/porta.py): corpo oltre 65 536 byte (64 KiB) rifiutato con 413 senza leggerlo tutto; Content-Length mancante 411; id_chiamante oltre 64 caratteri 422; id_chiamante con caratteri fuori dalla forma [A-Za-z0-9._:-] 400 id_chiamante_non_valido.

id_chiamante non è un campo libero, e non ci mettete segreti. Va a finire nel registro delle decisioni in chiaro, perché serve a ritrovare una chiamata dopo un guasto: è l'unico campo vostro che il servizio scrive così. Dal 26/10/2026 passa da una forma chiusa (lettere, cifre, . _ : -) proprio per non essere un posto dove un segreto possa finire per distrazione — ma la forma non vi impedisce di scriverci qualcosa che non volete sul nostro disco: quello dipende da voi. Misurato in F5g (docs/F5g-referto.md).

Se la chiamata è una prova, il contratto chiede che id_chiamante cominci per prova-: serve a chi legge i registri a distinguere l'esercitazione dal traffico vero (docs/F2-contratto.md §6-bis).

La risposta

Esito 200. Le chiavi sono sempre tutte presenti, sempre queste, mai una in più (CHIAVI_RISPOSTA in servizio/porta.py; tabella §1 di docs/F2-contratto.md):

chiavetiposignificato
versione_rispostastr"1"
id_decisionestrd- + 16 esadecimali, unico per chiamata
compitostrt1…t4
versione_schemastrversione dello schema servito
decisionestruna classe dell'elenco chiuso oppure ASTIENI
astenutobooltrue se e solo se decisione == "ASTIENI"
confidenzafloat|null0..1 calibrata; null quando il compito non è servito
sogliafloat|nullla soglia applicata a questa decisione
livellostr|nullL0, L1 o null (nessun livello ammesso)
motivo_astensionestr|nullvedi §4 qui sotto; null se non astenuto
a_vallestrcosa deve fare chi consuma: dal campo a_valle_astensione dello schema quando astenuto, dalla classe altrimenti
livello2obj|nullsolo t2: {"pacchetto":…, "confidenza":…, "soglia":…} o null
msfloattempo della decisione dentro il servizio
impronta_ingressostrsha256 del testo, primi 16 esadecimali

impronta_ingresso serve a ritrovare una decisione nei registri senza che il servizio conservi il vostro testo: il testo d'ingresso non viene mai scritto nei log.

Gli invarianti che il collaudo verifica su ogni risposta

Garanzie su cui potete costruire: servizio/collaudo.py le controlla su ogni risposta.

  1. decisione è una classe dello schema oppure ASTIENI — mai una classe inventata;
  2. astenuto è coerente con decisione;
  3. confidenza ≥ soglia quando non è astenuto (per i livelli che hanno una confidenza);
  4. nessuna chiave in più rispetto alla tabella qui sopra;
  5. il testo d'ingresso non compare in nessun campo della risposta.

L'invariante 5 è anche una cintura a tempo di esecuzione: se il motore rimettesse il testo dentro la risposta, la porta risponde 500 motore_incoerente invece di consegnarla.


4. L'astensione — la parte da capire bene

decisione: "ASTIENI" non è un guasto, non è un errore, non è un 500. È un esito normale, HTTP 200: il servizio ha guardato e non è abbastanza sicuro per decidere al posto vostro. Trattatelo come un caso previsto, non come un'eccezione.

Quando astenuto è true, motivo_astensione dice perché e a_valle dice in parole che cosa deve succedere adesso: leggete a_valle, è la parte del contratto scritta per voi. I motivi sono un elenco chiuso (§2 del contratto, MOTIVI_ASTENSIONE in servizio/porta.py): non ne arriveranno altri.

motivo_astensioneche cosa è successoche cosa dovete fare
compito_senza_livello_ammessonessun motore disponibile rispetta il vincolo di qualità che lo schema impone a quel compito. Il servizio si astiene sempre su quel compito, e lo dichiarausate il percorso che usate oggi senza AgileDecide: niente cambia
sotto_sogliail motore ha scelto una classe, ma con una confidenza sotto la soglia taratamandate il caso alla coda di revisione umana
nessuna_regola_scattatail livello a regole non ha trovato niente e il compito non ha una classe di ripiegorevisione umana
livello_ha_scelto_astieniil modello ha guardato e ha scelto di non decidere: su quel livello ASTIENI è una classe addestrata come le altrerevisione umana; è diverso da «non ho visto niente», e il registro lo distingue
troppo_lungoil testo supera lunghezza_max_caratteri dello schemaaccorciate, spezzate, o mandate a revisione umana
ingresso_vuototesto vuoto o fatto di soli spaziè un vostro difetto di integrazione: sistemate a monte
contesto_mancantemancano campi obbligatori dello schema dentro contestoaggiungete i campi dichiarati in campi_obbligatori

Due conseguenze pratiche: dimensionate la coda umana (il servizio è fatto per astenersi piuttosto che sbagliare, e oggi — vedi §8 — si astiene sempre), e non riprovate la stessa chiamata: salvo ingresso_vuoto e contesto_mancante, che sono difetti della richiesta, lo stesso testo dà lo stesso esito.


5. Gli errori

Il corpo di un errore è sempre {"errore": "<nome>"}, con un dettaglio opzionale che è un testo fisso: non contiene mai il vostro testo d'ingresso né la vostra chiave. La tabella è l'elenco completo dei casi ErroreRichiesta di servizio/porta.py.

HTTPerrorequando succedeche fare
400lunghezza_non_validaContent-Length non numerico, o negativodifetto del client HTTP
400json_malformatoil corpo non è JSON UTF-8 validosistemate la serializzazione
400id_chiamante_non_validoid_chiamante contiene caratteri fuori dalla forma ammessa (vedi §3)usate solo lettere, cifre e . _ : -
401chiave_assentemanca Authorization, non è Bearer, o il segreto è più corto di 32 caratterimettete la chiave; la risposta porta WWW-Authenticate: Bearer
401chiave_non_validala chiave non è nel registro, oppure è stata revocatachiedete una chiave nuova; non riprovate in ciclo
403chiamante_non_ammessostate chiamando da un indirizzo che non è fra i due ammessinon è un problema di chiave: non potete raggiungere il servizio da lì
403fuori_ambitochiave valida, ma il compito non è nell'ambito di quella chiavechiedete un ambito più largo, oppure chiamate solo i compiti del vostro ambito
404percorso_ignotoil percorso non esiste; oppure avete chiesto /healthz da fuori la macchinacontrollate la rotta
404compito_ignotoil compito non è nel registroleggete /v1/compiti
405metodo_non_ammessometodo sbagliato per quella rotta (es. GET /v1/systemone)la risposta porta l'intestazione Allow coi metodi giusti
411lunghezza_mancantela richiesta non porta Content-Lengthdifetto del client HTTP
413corpo_troppo_grandeil corpo supera 65 536 bytespezzate il testo
422campo_non_validoil corpo non è un oggetto; compito o testo mancanti o non stringhe; contesto non è un oggetto di stringhe; id_chiamante non stringa o oltre 64 caratteri; un campo non previstoil dettaglio dice quale campo
429quota_superatala vostra chiave ha superato il suo tetto di chiamate nella finestra in corsoaspettate i secondi dell'intestazione Retry-After e riprovate; vedi §8
500motore_incoerenteil motore ha risposto fuori dal contratto, o il testo d'ingresso è finito nella rispostasegnalatelo: è un difetto nostro, non vostro
500guasto_internoqualunque altro errore non previstoriprovabile; se persiste, segnalatelo

Prima di noi c'è il proxy, e ha esiti suoi. La tabella qui sopra è il servizio. Chiamando il nome pubblico, due di questi esiti arrivano dal proxy della regia senza che la richiesta ci raggiunga, e il corpo ha la stessa forma {"errore": "<nome>"}:

Il tetto per indirizzo è della regia, la quota per chiave è nostra: sono due limiti diversi, contati su cose diverse, e un 429 tetto_indirizzo non consuma la vostra quota (vedi §8).

Quante chiamate passano davvero, misurate. Su un secchio pieno (nessuna chiamata senza chiave nell'ultimo minuto) sono passate 6 chiamate di fila e le altre 7 hanno preso 429: una dentro il ritmo più le 5 della raffica. Non è un numero diverso dalla configurazione, è come la configurazione si vede da fuori; con il secchio già in parte consumato ne passano meno (la regia, dal suo posto, ne ha viste 5). Fonte: F5l, strumenti/collaudo_nome_pubblico.py, 13 chiamate senza Authorization il 26/10/2026.

Due esiti misurati dal nome pubblico che invece sono nostri, perché il percorso /v1/ il proxy lo inoltra: GET /v1/systemone risponde 405 (il verbo giusto è POST), ed è la riga metodo_non_ammesso qui sopra. Sulla lunghezza del corpo i due limiti si sommano nel verso giusto: il proxy taglia oltre 128 KiB, il nostro 413 corpo_troppo_grande scatta prima, a 64 KiB — chi sbaglia la dimensione prende quasi sempre il nostro.

Note sull'ordine, che conta quando più cose sono sbagliate insieme:

Ogni chiamata a /v1/systemone finisce nel registro delle decisioni anche quando finisce in errore, con l'esito HTTP. Il registro non contiene mai il vostro testo, né il contesto, né la chiave: solo impronta e lunghezza del testo e il nome della chiave (docs/F2-contratto.md §6).

Due cose che il collaudo di sicurezza di F5g ha cambiato il 26/10/2026, e vi riguardano:


6. La chiave e il suo ambito

Nel vostro codice la chiave va presa dall'ambiente o da un gestore di segreti, mai scritta nel sorgente. Negli esempi è $AGILEDECIDE_CHIAVE.


7. Esempi curl

export AGILEDECIDE_CHIAVE="…"   # dal vostro gestore di segreti, mai nel codice
export AGILEDECIDE_BASE="https://api.agiledecide.agile.software"
export AGILEDECIDE_CASA="http://127.0.0.1:8711"   # solo dalla macchina stessa: /healthz

L'elenco dei compiti

curl -sS "$AGILEDECIDE_BASE/v1/compiti" -H "Authorization: Bearer $AGILEDECIDE_CHIAVE"

Una decisione, con contesto (la forma che usa il collaudo in servizio/collaudo.py)

curl -sS -X POST "$AGILEDECIDE_BASE/v1/systemone" \
  -H "Authorization: Bearer $AGILEDECIDE_CHIAVE" -H "Content-Type: application/json" \
  -d '{"compito":"t3","testo":"misura della porta, testo corto",
       "contesto":{"prodotto":"agiletav"},"id_chiamante":"prova-1"}'

Una decisione senza contesto, e la risposta che il servizio dà oggi (vedi §8): esito 200, astensione dichiarata.

curl -sS -X POST "$AGILEDECIDE_BASE/v1/systemone" \
  -H "Authorization: Bearer $AGILEDECIDE_CHIAVE" -H "Content-Type: application/json" \
  -d '{"compito":"t2","testo":"dove clicco per cambiare la password?"}'
{
  "versione_risposta": "1", "id_decisione": "d-0123456789abcdef",
  "compito": "t2", "versione_schema": "2.3",
  "decisione": "ASTIENI", "astenuto": true,
  "confidenza": null, "soglia": null, "livello": null,
  "motivo_astensione": "compito_senza_livello_ammesso",
  "a_valle": "In entrambi i casi la richiesta prende il percorso di serie …",
  "livello2": null, "ms": 0.0142, "impronta_ingresso": "0123456789abcdef"
}

Il corpo di un errore, col codice HTTP; e lo stato del processo, che si legge solo dalla macchina stessa e senza chiave:

curl -sS -w '\nHTTP %{http_code}\n' -X POST "$AGILEDECIDE_BASE/v1/systemone" \
  -H "Authorization: Bearer $AGILEDECIDE_CHIAVE" -H "Content-Type: application/json" \
  -d '{"compito":"t9","testo":"ciao"}'
# {"errore": "compito_ignoto", "dettaglio": "compito fuori dal registro"}  /  HTTP 404

curl -sS "$AGILEDECIDE_CASA/healthz"

8. I limiti di oggi, detti chiaramente

Due limiti, non uno. Davanti alla quota per chiave, dal nome pubblico, c'è il tetto per indirizzo del proxy della regia: 10 chiamate al minuto (raffica 5) per chi chiama senza Authorization: Bearer con un segreto di almeno 32 caratteri, e 5 al secondo (raffica 20) su tutte le chiamate; oltre il tetto, 429 tetto_indirizzo con Retry-After: 60 (§5). Quello non è nostro e non lo governiamo: non guarda la vostra chiave, guarda l'indirizzo da cui arrivate, e per questo più vostri processi che escono dallo stesso indirizzo lo consumano insieme. La quota per chiave qui sotto è nostra, ed è cosa diversa: si conta sull'impronta della chiave, e oggi è il solo limite che il servizio tiene da sé.

Quote per chiave. Il meccanismo è nel codice (servizio/porta.py, classe Quote):

I numeri sono per singola chiave e stanno nel registro delle chiavi, campi quota_chiamate e quota_finestra_s: ve li dice chi vi conia la chiave, perché non sono gli stessi per tutti. Oggi nel registro ci sono due voci, e questi sono i loro numeri veri, letti da servizio/chiavi.json il 26/10/2026:

voceambitoquotastato
serverino-regiat1 t2 t3 t4nessuna quota dichiarata → nessun limiteattiva
agile-servicest1 t2 t3 t460 chiamate ogni 3600 s (un'ora)attiva

La voce agile-services è la chiave di prova, e dal 26/10/2026 è attiva: il segreto l'ha coniato chi tiene il caveau e non è mai passato da questo box, che ha ricevuto solo i 64 esadecimali della sua impronta. La chiave in chiaro la consegna il caveau al cliente, non noi: qui non c'è nessuna copia da farsi dare. Finché una voce resta senza impronta (stato in_attesa_di_impronta, come era questa fino al 26/10) nessuna chiave al mondo autentica su di essa.

Il suo ambito sono tutti e quattro i compiti, e non è una scelta larga: è l'elenco dei compiti che servizio/registro.json dichiara non serviti, cioè quelli in prova. La voce porta per questo il campo classe: prova, e la classe non è un'etichetta: il servizio rifiuta di caricare una voce di classe prova che abbia nel suo ambito un compito passato in esercizio. Il giorno in cui uno dei quattro verrà servito da un livello nostro, questa chiave non lo raggiungerà: il servizio lo dirà invece di lasciarla passare in silenzio. Una voce senza campo classe vale esercizio e non ha questo vincolo.

Perché 60 all'ora: è una dose da cliente che prova. Bastano per provare a mano tutte le rotte e un lotto di una cinquantina di testi in un'ora; e un ciclo impazzito costa al massimo 60 chiamate, contro le migliaia all'ora che la porta regge.

Oggi i quattro compiti rispondono tutti ASTIENI. Non è un guasto, ed è dichiarato: in servizio/registro.json ogni compito ha "servito": false e la risposta porta motivo_astensione: "compito_senza_livello_ammesso". Il motivo è lo stesso per tutti e quattro: lo schema di ciascun compito impone un vincolo di qualità, e nessuna configurazione misurata lo rispetta in modo dimostrato.

compitovincolo che non si tienecome sta oggi (campo perche di servizio/registro.json)
t1 — cancello dei dati personalirichiamo su BLOCCA ≥ 0.98tenuto solo da una configurazione ospitata fuori, che per questo compito non si può usare: l'ingresso di t1 è esattamente il testo che non deve uscire. Sui nostri processori il meglio a registro è sicurezza 0.7729
t2 — instradamentoprecisione su NORMATIVA ≥ 0.90gli altri due vincoli sono tenuti, questo no: il punto più alto è 1.000 su 56 decisioni, ma l'intervallo di confidenza si ferma a 0.851
t3 — posta in arrivorichiamo su PRATICA_URGENTE ≥ 0.98, precisione su INDESIDERATA ≥ 0.98, zero errori da NOTIFICA_AUTOMATICAnessuno dei tre dimostrato; l'ultimo è mancato, non solo non dimostrato
t4 — prossimo gesto della persona digitalerichiamo su PASSA_A_PERSONA ≥ 0.98 e altri duenessuno dimostrato; il braccio migliore decide meno dell'8% del traffico

Cosa vuol dire per voi, in pratica:


9. Scostamenti fra contratto e codice

Trovati confrontando ogni ErroreRichiesta di servizio/porta.py con docs/F2-contratto.md. Esiti presenti nel codice che il contratto non nomina (§5 elenca solo 401/401/403/404, §4 il 413, §0 il 403 del chiamante e il 404 di /healthz):

HTTPerrorenota
400lunghezza_non_validaContent-Length non numerico o negativo
400json_malformatocorpo non JSON UTF-8
404percorso_ignotoil contratto lo nomina solo per /healthz da fuori; nel codice vale per qualunque rotta sconosciuta
405metodo_non_ammessocon intestazione Allow
411lunghezza_mancanteil contratto cita il limite di 64 KiB ma non il caso «Content-Length assente»
422campo_non_validotutta la validazione del corpo; il contratto §4 descrive i campi ma non l'esito
500motore_incoerentecintura su §1: risposta del motore fuori contratto, o testo d'ingresso dentro la risposta
500guasto_internoqualunque eccezione non prevista

Altri scostamenti, non di esito:

  1. Il contratto non descrive la forma del corpo d'errore ({"errore": …, "dettaglio": …}), né le intestazioni WWW-Authenticate: Bearer sul 401 e Allow sul 405. Sono nel codice (Gestore._errore, Gestore._smista in servizio/porta.py) e in questa guida.
  2. Chi serve cosa. La tabella §3 del contratto dà t2 servito da L0 a soglia 0.70; il dato vivo servizio/registro.json, più recente, dice "servito": false per tutti e quattro i compiti. Vale il registro: il contratto stesso dichiara che «il registro dei compiti è un dato, non codice». Questa guida segue il registro.
  3. Motivi di astensione oggi irraggiungibili. Non essendo servito nessun compito, il cancello in servizio/motore.py esce subito con compito_senza_livello_ammesso: gli altri sei motivi di §2 restano nel contratto e provati dalle batterie interne, ma non possono arrivare a un chiamante. Sono documentati lo stesso in §4: la vostra integrazione deve reggerli il giorno in cui un compito si accende.
  4. La quota, scostamento CHIUSO (verificato il 26/10/2026, F5h). Questo punto diceva «la quota non è nel contratto: il contratto va aggiornato», e da A847 non è più vero: docs/F2-contratto.md §5-bis — la QUOTA per chiave nomina il tetto per chiave, il 429 quota_superata e l'intestazione Retry-After (riga 147), e §6-ter mette la quota a contrasto col credito del registro. Resta nel codice solo l'attuazione (servizio/porta.py, Quote) e nei campi quota_chiamate / quota_finestra_s del registro delle chiavi. Il punto si tiene qui, chiuso e datato, invece di sparire: un elenco di scostamenti da cui le righe svaniscono non si può rileggere.