Validazione documentale automatica
Una pipeline fra tre sistemi, dove i modelli leggono e il codice decide
- Periodo
- dal 2025
- Ruolo
- progettazione, sviluppo, test e definizione del contratto
- Tecnologie
- Python · Google Cloud Vision · GPT-4o · modelli di visione · ExifTool · PyMuPDF · n8n
- Stato
- in produzione
a cosa serve
Chi rendiconta un progetto carica documenti: fotografie che devono provare un'attività avvenuta in un certo luogo e in una certa data, scontrini e ricevute che devono diventare righe di spesa. Verificarli a mano significa aprirli uno per uno, leggere date e importi, trascriverli e confrontarli con quello che il progetto dichiara. È il tipo di lavoro in cui un errore non si vede: una data fuori periodo o un importo trascritto male non hanno un aspetto diverso da un dato giusto.
Questa pipeline lo fa al posto della persona, e usa tre modelli per farlo: uno che legge le parole da un'immagine, uno che guarda una fotografia e ne descrive il contenuto, uno che interpreta i campi rimasti ambigui nel testo. Il risultato torna al gestionale come un insieme di semafori e di righe precompilate: chi controlla vede subito dove guardare, invece di aprire tutto.
La decisione, però, resta fuori dalla portata dei modelli. Il servizio produce fatti — cosa c'è scritto sul documento, cosa dicono i metadati della fotografia, dove il dato non torna col contesto dichiarato — e non un giudizio di ammissibilità. Quello lo calcola il gestionale, in modo deterministico. È il vincolo attorno a cui è costruito tutto il resto, e la ragione per cui una pipeline che usa tre modelli resta verificabile da una persona.
L'ho progettata e sviluppata da solo, contratto fra i sistemi compreso.
come è fatta
Tre sistemi in fila, e un quarto elemento che non esegue niente ma tiene insieme gli altri: un repository fatto di soli contratti, senza una riga di codice applicativo.
Al caricamento di un file parte un'elaborazione asincrona, e il risultato torna al gestionale come callback autenticato. Da lì diventa un sistema di semafori nell'interfaccia: colori che dicono, per ogni documento, quali controlli sono passati e quali no.
il contratto è la sostanza
Il lavoro che pesa di più non è nessuno dei tre stadi: è aver scritto il contratto fra i due sistemi che si scambiano l'analisi. Quali campi restituisce il servizio, cosa può contenere ciascun blocco, cosa il gestionale può aspettarsi di trovare quando legge il callback, e cosa succede quando arriva un valore che non conosce.
Un contratto del genere nasce di solito da una trattativa fra chi scrive i due lati. Qui l'ho scritto da solo, per sistemi su cui intervengo io stesso: è una scelta di progettazione, guardando insieme i vincoli di chi produce il dato e di chi lo consuma, e non la mediazione fra due parti che difendono interessi diversi. Il rischio di questa condizione è noto — chi scrive tutti e due i lati può far combaciare le cose per come le ha in testa oggi — e lo contengo tenendo il contratto in un repository suo, dove una modifica si scrive prima di essere implementata da una delle due parti.
il confine passa fra i fatti e la decisione
La regola che governa tutto il documento è che il produttore non emette giudizi, e le sue conseguenze sono più concrete di quanto sembri.
La classificazione della natura di una spesa, per esempio, è una proposta: il servizio dice se una riga somiglia a un alloggio, a un viaggio, a un pasto, o dichiara di non saperlo. Non emette i codici del database del gestionale, e non perché sia scomodo: una scelta di quel dominio non deve attraversare il confine. Tradurre la proposta nel vocabolario interno è responsabilità di chi consuma, e il valore «non lo so» non ha corrispondente — resta all'operatore.
Allo stesso modo un campo che il servizio non è riuscito a determinare arriva con un valore esplicito di non-determinato, e non semplicemente vuoto o assente: «non l'ho letto» è un'informazione diversa da «non c'è», e il consumatore la tratta come vuoto ma con priorità di revisione.
due famiglie di avvisi, che dicono cose diverse
Sono la parte del contratto che uso come esempio quando devo spiegare cosa distingue un contratto scritto da uno improvvisato.
| Famiglia | Cosa dice | Cosa implica |
|---|---|---|
| Avvisi di lettura | quanto il dato è certo: più date rilevate e presa la prima, più importi vicino a una parola-totale, valuta dedotta dal contesto invece che letta | il dato potrebbe essere sbagliato |
| Avvisi di coerenza | il dato è letto bene, ma non torna col contesto: data fuori dal periodo dichiarato, valuta diversa da quelle attese, città diversa da quella dell'evento | il documento potrebbe non appartenere a questa trasferta |
Tenerle separate serve a chi legge: un avviso di lettura mette in dubbio il numero, uno di coerenza mette in dubbio l'appartenenza del documento. Nessuna delle due invalida la riga — sono un invito alla revisione umana, non un esito — e gli avvisi di coerenza vengono emessi solo quando il confronto è davvero possibile: un campo non determinato o un contesto senza evento non producono incoerenze inventate.
la lezione sul versionare: una garanzia vale finché regge la sua premessa
Un giorno ho cambiato il significato di un codice di avviso senza aumentare il numero di versione del contratto. La motivazione non era pigrizia ed era verificata sul codice dell'altro lato: nessun ramo del consumatore dipendeva dal valore di quel codice — li univa tutti in una nota di testo e conservava il payload intero. Con quella premessa, cinque codici in più o un significato diverso non rompevano niente.
Ho scritto nel contratto anche la condizione a cui quella garanzia era appesa: vale finché resta vero che nessuno discrimina. È caduta lo stesso giorno, la sera: il consumatore ha iniziato a usare i codici per comporre la nota all'operatore. Nessuna decisione di business dipende ancora da un codice, ma la premessa non c'è più — quindi da quel momento la semantica dei codici è congelata, e cambiarne uno esistente richiede il numero di versione nuovo. Aggiungerne resta additivo, perché il consumatore elenca in coda i codici che non conosce invece di ignorarli.
La lezione non è sul versionamento in sé: è che una deroga si può concedere solo se si scrive accanto la condizione che la rende innocua, altrimenti sopravvive alla ragione per cui era stata accettata.
dalla foto ai fatti
Il percorso delle fotografie non usa affatto la lettura del testo: lavora sui metadati che la macchina fotografica scrive dentro il file, e serve a rispondere a due domande — questa foto è stata scattata quando dice il progetto, ed è stata scattata dove dice il progetto.
Le scelte che contano sono quattro, e tutte hanno a che fare col non spacciare per prova qualcosa che non lo è.
- La data di modifica del file non si usa mai. Sarebbe sempre disponibile e sembrerebbe una data: è invece la data dell'ultima copia, e su una foto scaricata da una chat coincide col giorno in cui è stata scaricata. Il servizio distingue le date originali dello scatto dalle date secondarie e dichiara quale ha usato, con l'etichetta della sorgente che l'ha prodotta. Meglio nessuna data che una data verosimile e falsa.
- La coerenza fra orario dello scatto e orario del GPS si controlla degradando, non fallendo. Se ci sono entrambi gli orari completi si confrontano in fuso orario assoluto, con una tolleranza di due ore; se manca l'ora del GPS si scende al confronto per giorno solare; se non c'è abbastanza informazione l'esito non è «incoerente», è «non valutabile», e la differenza viene dichiarata. Un controllo che sotto una certa qualità del dato smette di pronunciarsi è più utile di uno che tira a indovinare.
- La qualità è un giudizio tecnico, non un esito di business. Il servizio marca i metadati come attendibili, sospetti o assenti, e allega sempre le ragioni: manca la data, è stata usata una data secondaria, manca il dispositivo, il GPS è incompleto, gli orari non tornano. Chi decide se il documento vale resta il gestionale, che quei segnali li usa come vuole.
- La traccia di un software di editing non basta a rendere sospetta una foto. Prima lo faceva, e produceva sospetti su fotografie perfettamente buone: aprire un'immagine con un'applicazione qualunque lascia quella traccia. Ora resta un segnale registrato, ma il sospetto lo fanno scattare solo le cose che riguardano la prova — data mancante o secondaria, dispositivo assente, GPS incompleto o incoerente.
La lettura del file e la risoluzione delle coordinate in un indirizzo sono due passi deliberatamente separati: il primo è deterministico e ripetibile, il secondo dipende da un servizio di terzi, può mancare, e porta con sé l'attribuzione della fonte. Tenerli insieme avrebbe reso non ripetibile anche la parte che non ha ragione di esserlo.
due fasi, e settantun secondi che non servivano a niente
Il percorso degli archivi di foto ha una struttura a due fasi che è nata da un difetto pagato in produzione, e lo racconto perché il modo in cui è stato corretto conta più del difetto.
Una validazione su un archivio di trentadue foto era rimasta appesa. La richiesta era durata circa centoventicinque secondi, e settantuno di quei secondi erano serviti a caricare le foto sul fornitore del modello, una alla volta — in una modalità in cui il modello non viene mai chiamato. I file venivano caricati, non usati da nessuno, e cancellati a fine elaborazione. Il tempo perso ha fatto sfondare il limite del trasporto, che ha chiuso la connessione pochi secondi prima che il servizio finisse.
Due cose ho voluto scrivere per intero nella decisione. La prima è da quando a quando: il difetto era entrato tre settimane prima, con il lavoro che introduceva il caricamento unico delle immagini, e in quella finestra ogni elaborazione di soli metadati ha caricato l'intero archivio su un fornitore esterno senza che nessun modello lo guardasse. È il genere di data che serve avere quando qualcuno chiede da quando a quando, e non è ricostruibile a posteriori se non la si scrive subito.
La seconda è perché la correzione non è stata un controllo in più. Il varco mancante era uno solo e una riga lo avrebbe chiuso. Ma la ragione per cui mancava è che l'ordine del codice non diceva chi dipendesse da cosa: il caricamento stava in cima a tutto, mentre le due vere chiamate al modello erano già condizionate più in basso. Chi l'aveva scritto l'aveva messo dove stava l'estrazione dell'archivio — il posto giusto per estrarre, quello sbagliato per una chiamata di rete. Ora le due fasi sono esplicite e la fase dei soli metadati non riceve nemmeno il client del fornitore: non ha proprio il modo di contattarlo, invece di doverlo evitare per disciplina di chi legge il codice. La stessa svista, oggi, costa un errore nei test invece di settantun secondi in produzione.
quando un file non si può analizzare, si dice cosa fare
Un archivio poteva arrivare pieno di file in formato RAW — quelli che alcuni telefoni salvano accanto al JPEG — e tornare al gestionale come archivio vuoto, senza dire perché. Nella schermata non compariva nulla di utile, e la conclusione di chi guardava era che il sistema non funzionasse.
Due decisioni, entrambe piccole e con effetti sproporzionati.
Il controllo sul formato viene prima di quello sulla dimensione. Un file RAW da cinquanta megabyte sfora entrambi i limiti, e scartarlo come «troppo grande» è vero e inutile: chi legge lo comprime, riprova, e sbatte contro il formato con un errore diverso. Fra due ragioni entrambe corrette si dichiara quella su cui l'utente può agire.
E la ragione di scarto porta la via d'uscita, non solo il rifiuto: quali formati sono accettati, e come si esporta un RAW in un formato che conserva i metadati originali. Un motivo di scarto non è un codice di errore per chi sviluppa — è l'unica cosa che l'utente legge, e deve permettergli di risolvere senza aprire una segnalazione. Il testo è lungo per questo.
C'è anche un effetto sui conteggi che ho dichiarato perché cambia i numeri che qualcuno potrebbe confrontare: un RAW piccolo prima passava, veniva «elaborato» e produceva un esito negativo muto; ora è scartato come quello grande, con la stessa spiegazione. Si sposta dalla colonna degli elaborati a quella degli esclusi, ed è la classificazione onesta — un file che non sappiamo analizzare è escluso, non analizzato male.
dalla pagina alle righe di spesa
Il percorso più interessante è quello delle note spese: una pagina scansionata con più scontrini attaccati deve diventare una riga per ogni spesa. È il caso in cui la domanda «dove sta l'AI in questo sistema» ha una risposta precisa.
la separazione viene dopo la lettura, e non era il piano
Sulla carta il modulo ha una fase di segmentazione e poi una di lettura: prima si ritagliano i documenti dalla pagina guardando i pixel, poi si legge ciascuno. Nei fatti la segmentazione a pixel cerca bande scure fra un documento e l'altro e sui documenti veri non ne trova mai, perché gli scontrini sono carta chiara su fondo chiaro. Passa quindi la pagina intera al lettore, e i documenti si separano dopo, sulle coordinate delle parole.
Non è un ripiego, ed è diventato il vincolo che governa ogni scelta futura di motore. Le coordinate sono un dato più affidabile dell'intensità dei pixel e non costano nulla in più: arrivano insieme al testo, con una sola chiamata. Ma qualunque alternativa dovrà restituire testo e geometria: senza riquadri salta la separazione, e una pagina con tre scontrini torna a produrre una riga sola. È anche il motivo per cui un modello linguistico generico non può fare da lettore di riserva — restituisce testo, non coordinate.
quando un blocco di testo è un documento
È il cuore della separazione, ed è un criterio semantico, non geometrico. Il motivo è misurato: dentro un singolo scontrino i vuoti fra intestazione, corpo e piede arrivano a 4,0 altezze-riga, mentre il vuoto fra due scontrini impilati scende a 3,2. Le due distribuzioni si sovrappongono, quindi nessuna soglia di spazio bianco separa i documenti da sola.
Il codice taglia allora volutamente fitto, alternando i due assi perché due scontrini affiancati ma sfalsati non si separano con una proiezione sola, e poi ricompone: un blocco vale come documento se contiene un importo e, insieme, una parola-totale o una data. Tutto il resto — carta intestata, piè di pagina, condizioni contrattuali — viene riattaccato al documento adiacente.
Il criterio serve in tutte e due le direzioni, e va letto così. Senza la richiesta dell'importo, il blocco di condizioni di un biglietto aereo diventerebbe una riga di spesa fantasma. Con essa, un documento vero che il criterio non riconosce viene declassato a frammento e riassorbito nel vicino: è così che nasce una fusione, e il testo finisce nella riga sbagliata invece di sparire.
il caso che ha mostrato dove sta la fragilità
Il criterio dipende da una lista di parole e da una forma di numero: è conoscenza, e la conoscenza può mancare. Sulle note spese in cinese il motore falliva sistematicamente, per due ragioni indipendenti scoperte misurando.
- Le parole-totale non c'erano nella lista, che conosceva la forma giapponese e non le tre cinesi. In più i termini in ideogrammi non tolleravano lo spazio fra un carattere e l'altro: il criterio legge il testo ricomposto dalle coordinate, e il lettore restituisce i due ideogrammi come parole distinte — quindi al confronto arrivava una forma spaziata, e nemmeno il termine giapponese agganciava mai.
- In quella valuta l'importo si stampa intero e senza simbolo. Il modello di riconoscimento degli importi pretendeva decimali o migliaia raggruppate, e quello degli importi nudi pretendeva un simbolo di valuta: comande da cento parole risultavano prive di importo. La forma nuova ammette la cifra intera solo se marcata dalla parola-totale, che è lo stesso principio con cui già si accettava un importo preceduto dal simbolo.
Il risultato: documenti persi dal 36,1% al 25,0%, righe attribuibili con certezza da 50 a 71, e ogni campo migliora in assoluto — data e totale da 41 e 42 letture corrette a 63.
La lezione generale conta più del cinese: il motore non era al suo limite, era cieco su una lingua. Quello che si è tolto è un difetto, non un vincolo, e il tetto vero dell'approccio deterministico resta quindi ignoto.
come si misura
Un comando esegue la pipeline vera su un campione annotato — 108 documenti su 37 pagine, da note spese reali in più lingue e valute — abbina ogni riga prodotta al documento che le corrisponde, e stampa le metriche. L'esito della lettura è tenuto in cache su disco, così separazione, estrazione e istruzioni al modello si possono rieseguire senza una sola chiamata a pagamento: la variante «solo regole» è gratuita e deterministica.
La separazione ha quattro esiti, non due, e la distinzione è ciò che rende la misura utile:
| Esito | Perché conta |
|---|---|
| Fuso | due documenti in una riga sola: chi controlla se ne accorge, il totale è ambiguo |
| Perso | nessuna riga lo copre: una spesa sparisce senza traccia |
| Eccesso | un documento spezzato in due righe: produce spese che non esistono, e nessuno se ne accorge sommando |
| Riga fantasma | non corrisponde a niente |
L'esito «eccesso» è il motivo per cui un motore che migliora il conteggio medio va scartato lo stesso se sbaglia per eccesso. Un conteggio non lo mostra; questa colonna sì.
Sui campi gli esiti sono quattro per la stessa ragione: corretto, sbagliato, mancato — il documento lo dice, il motore no, e si vede — e inventato: il documento non lo dice e il motore risponde comunque, e non si vede. Astenersi dove il documento tace conta come risposta corretta, altrimenti un motore che tira a indovinare batterebbe uno che si astiene.
la colonna che ha cambiato le priorità
È «inventato» ad aver rivelato il difetto più grave, che nessun'altra misura avrebbe visto. Sulla città il modello produceva 17 valori inventati su 50, e non a caso: erano tutti la città dell'evento a cui la trasferta si riferiva. Su una ricevuta di taxi che l'indirizzo lo stampa per esteso, lo sovrascriveva con la città dell'evento — e nelle due scansioni della stessa pagina scriveva due città diverse, quindi non era nemmeno coerente con sé stesso.
La spiegazione sta nel diagramma qui sopra: quel modello non vede l'immagine, e una città stampata dentro un logo non gli arriva affatto: quel dato è fuori dal suo ingresso, quindi non c'è modo che lo legga. E quando non lo trova nel testo, tende a ripetere il contesto che gli è stato dato.
La città è stata tolta al modello invece di correggerlo con istruzioni più severe: da 27 letture corrette e 17 invenzioni si è passati a 35 corrette, zero sbagliate e una sola invenzione. Quando un componente non ha in ingresso l'informazione che gli si chiede, il rimedio non è chiedergliela meglio.
Insieme alla domanda è uscito anche il suggerimento: la città dell'evento non compare più nemmeno fra i riferimenti passati al modello, perché chiedere un dato e suggerirlo sono lo stesso canale. E una cosa peggiora, dichiarata: le astensioni sulla città passano da 4 a 14 su 50 righe. È peggio in apparenza e meglio in sostanza — un'astensione si vede in revisione, una città sbagliata no, e un avviso di incoerenza calcolato su una città inventata manda chi controlla a cercare un problema che non esiste.
i numeri di oggi
| Misura | Prima | Oggi |
|---|---|---|
| Documenti persi (fusi o non letti) | 39 — 36,1% | 27 — 25,0% |
| Righe separate bene e attribuibili | 50 | 71 |
| Data · totale corretti | 41 · 42 | 63 · 63 |
| Valuta · città corretti | 45 · 35 | 62 · 49 |
| Natura della spesa corretta | 47 | 68 |
| Chiamate al modello · tempo | 142 · 241 s | 155 · 177 s |
Due limiti del banco, da sapere prima di citarne un numero. La verità di riferimento è stata redatta leggendo le pagine e ratificata da una persona, non da un oracolo indipendente: un errore sistematico condiviso fra chi annota e la pipeline sarebbe invisibile. E non esiste un sottoinsieme di controllo — i difetti sono stati cercati sugli stessi documenti su cui si misura il miglioramento. Su un fatto di lingua il rischio è basso, ma la disciplina corretta è tenere da parte alcune pagine annotate e non guardarle mai.
C'è anche una conseguenza contro-intuitiva che vale la pena dichiarare: separare meglio i documenti aumenta il costo, perché si paga a riga e le righe diventano di più. Le chiamate sono salite da 142 a 155 proprio perché la separazione è migliorata.
prima si misura, poi si sostituisce
Il principio che governa gli sviluppi: prima si misura il motore che già gira, poi si affianca un'alternativa. Senza un riferimento numerico, sostituire uno stadio è un atto di fede e il risultato si giudica guardando le immagini, che è esattamente ciò che si voleva smettere di fare. Ogni fase ha una soglia di passaggio, e alla successiva si procede solo se la misura della precedente lo giustifica.
- Misurare il motore in carica — fatta. La soglia era il 5% di documenti persi; la prima misura ha dato 36,1%. Allargare il campione ha peggiorato il numero, ed era il punto: un campione senza casi difficili misura tutti i motori come equivalenti.
- Affiancare un modello che guarda il foglio — scritta, sospesa. Dopo la correzione la perdita è scesa al 25,0%, e la composizione di ciò che resta non contiene un solo caso che chieda un segmentatore visivo: sono carte d'imbarco senza importo, documenti ruotati, documenti sovrapposti. Problemi di foglio, non di algoritmo. Da misurare, in quel caso, sarebbe stata anche la stabilità: un segmentatore che dà tre documenti oggi e due domani sullo stesso file non è utilizzabile dove qualcuno firma una nota spese, e un conteggio medio non lo mostra.
- Un servizio specializzato di terzi — condizionata. Il prezzo non decide, ai volumi in gioco sono decine di euro l'anno. Decide se separa più documenti sulla stessa pagina, e soprattutto il perimetro dei dati: mandare documenti reali a un fornitore esterno è una decisione dichiarata aperta, non un dettaglio implementativo.
La fase sospesa si riapre a una condizione precisa, ed è l'unica cosa che la geometria non potrà mai coprire: leggere la città dai loghi, che al modello testuale non arriva.
l'input è una variabile del risultato
Siccome la separazione è geometria, la qualità della scansione conta quanto il codice. La composizione dei documenti ancora persi lo dice senza margini: nessuno di essi chiede un lettore migliore. Da qui quattro regole di acquisizione, che costano una pagina di istruzioni invece di un modello nuovo.
- Solo documenti di spesa. Una carta d'imbarco non stampa alcun importo: nessun motore la riconoscerà mai come documento di spesa, perché non lo è — occupa spazio, si accosta a uno scontrino vero e se lo tira dentro.
- Niente pagamenti che duplicano una ricevuta già presente. Se lo scontrino della carta e la ricevuta della stessa transazione stanno entrambi sul foglio, il caso migliore è che il motore li fonda; il caso peggiore è che li separi bene, perché produce due righe dove la spesa è una.
- Tutti nello stesso verso. Una pagina a orientamento misto costa due volte: il lettore sceglie un layout e ignora il resto, e dentro quella fascia i documenti ruotati non si separano fra loro.
- Margine bianco e nessuna sovrapposizione, più largo dell'interlinea di uno scontrino: non è una cifra arbitraria, è il criterio che il codice usa davvero.
Sommando le quattro regole la soglia del 5% sarebbe raggiungibile senza aggiungere un modello. Ma è una stima da geometria e annotazione: nessuno ha ancora riscansionato quelle pagine seguendo le regole, quindi resta un'ipotesi con dei numeri dietro, non un risultato.
limiti dichiarati
- Le righe fuse arrivano in silenzio. La pipeline oggi non dichiara di aver avuto difficoltà: densità dei token, blocchi scartati e orientamenti misti sono segnali che il codice già calcola, e farglieli dire è il seguito naturale. Non è implementato.
- Due documenti ruotati nella stessa fascia non si separano fra loro, perché il testo si ricostruisce per righe orizzontali.
- La pipeline lavora per pagina e non ricuce un documento che sta su due fogli.
- Un conto diviso su due carte attribuisce più di quanto è stato speso: il documento dichiara entrambe le righe di pagamento, il campo del totale no. Tocca all'interfaccia di revisione renderlo evidente.
- Due classificazioni che il servizio non sa risolvere, dichiarate ma non misurate: la fattura d'albergo che comprende i pasti, che è un documento solo con due nature di spesa, e i biglietti ferroviari, dove il confine fra viaggio e trasporto locale dipende dalla tratta più che dal mezzo. Nessuno dei due è comparso nel campione: sono limiti dichiarati per onestà, non osservati.
- Il tempo non scende migliorando la separazione, semmai sale. L'unica leva è rendere parallele le chiamate, oggi in fila: è anche ciò che sblocca l'uso di un modello più capace, già cablato dietro un interruttore e spento perché in sequenza sfonderebbe il limite di tempo del trasporto.
- Il caricamento delle immagini resta seriale, quindi un archivio grande in modalità completa supera ancora il limite di tempo del trasporto. È un problema del trasporto più che del servizio, e sta scritto fra i lavori aperti.
quello che resta fermo
Intorno a tutto il flusso valgono alcune proprietà fisse, ed è la parte che dice fin dove un modello può arrivare dentro un processo che deve restare verificabile anche dopo che ha risposto.
I modelli osservano ed estraggono; la decisione sull'esito della validazione resta fuori dalla loro portata. I numeri economici restano calcolati in modo deterministico nel gestionale. Il livello di trasporto si limita a spostare la richiesta, senza applicare logica propria. Un callback che fallisce non deve lasciare un record a metà, in attesa di un esito che potrebbe non arrivare più.
stato
In produzione. Il campione annotato e i documenti di collaudo non sono versionati — sono note spese reali — e senza quei file il banco di prova non gira: dà un errore esplicito, non un risultato vuoto.