Files
ThothII/docs/plans/2026-08-24-evidence-restructuring-design.md
T

910 lines
38 KiB
Markdown

# Ristrutturazione delle Evidence — disegno approvato
**Stato:** approvato il 24 agosto 2026
**Sostituisce:** `docs/plans/2026-08-18-evidence-canonica-design.md`
**Ambito:** authoring, revisione, pubblicazione, indicizzazione e uso runtime delle Evidence
## 1. Obiettivo
Questo disegno introduce un processo semplice e verificabile per trasformare documenti
di partenza non necessariamente ben organizzati in Evidence strutturate, revisionabili
da una persona e ricercabili in modo efficace da ThothII.
La soluzione deve:
1. partire dai testi oggi presenti nel repository del workspace;
2. riorganizzarli senza inventare informazioni;
3. conservare sorgenti e risultato nello stesso repository Git;
4. affidare a Git la revisione e l'approvazione umana;
5. indicizzare soltanto le versioni approvate;
6. sfruttare Qdrant senza moltiplicare collezioni e componenti;
7. inserirsi nel workflow modulare attuale, nel quale Evidence è un modulo autonomo.
La fonte di verità rimane sempre il repository Git. Qdrant è un indice derivato che può
essere ricostruito.
## 2. Principio guida
Il processo è diviso in due percorsi distinti.
- Il **percorso di authoring** prepara e revisiona le Evidence fuori dalle sessioni
domanda→SQL.
- Il **percorso runtime** è in sola lettura e consulta esclusivamente Evidence già
pubblicate.
```mermaid
flowchart LR
S["Testi sorgente"] --> P["Pre-processing"]
P --> C["Evidence curate"]
C --> R["Revisione Git umana"]
R --> M["Merge e attivazione revisione"]
M --> I["Indicizzazione atomica"]
I --> Q["Qdrant: indice attivo"]
Q --> E["Evidence Module"]
E --> W["Workflow F1-F8"]
```
Una sessione può proporre una nuova formula o segnalare una lacuna, ma non modifica il
repository e non pubblica autonomamente conoscenza.
## 3. Struttura nel repository del workspace
Ogni workspace adotta questa struttura sotto la propria directory `evidence/`:
```text
evidence/
├── README.md
├── source/
│ └── ... documenti originali ...
├── curated/
│ ├── glossary/
│ ├── domain/
│ ├── enum/
│ ├── example/
│ ├── mapping/
│ ├── normalization/
│ ├── formula/
│ └── reference/
├── manifest.yaml
└── evaluation.yaml
```
### 3.1 `source/`
Contiene i documenti originali. La prima versione accetta file Markdown, testo UTF-8 e
file `.sql.md`. Un URL può essere descritto in un documento, ma non viene scaricato né
interpretato automaticamente.
I sorgenti vengono preservati: il pre-processing non li riscrive.
### 3.2 `curated/`
Contiene una Evidence Unit per file. Le sottodirectory rendono immediatamente visibile
il tipo anche a un lettore umano. Il campo `kind` nel documento resta comunque
obbligatorio: la directory aiuta la navigazione, il campo è il contratto macchina.
### 3.3 `manifest.yaml`
È gestito dal comando di preparazione e registra:
- hash di ciascun sorgente;
- Evidence Unit derivate da quel sorgente;
- identificatori stabili;
- versione del processo di preparazione;
- unità orfane da controllare.
Il manifest permette di elaborare soltanto ciò che è cambiato. Non sostituisce Git e
non contiene lo stato di approvazione.
### 3.4 `evaluation.yaml`
Contiene inizialmente circa venti domande rappresentative e gli identificatori delle
Evidence che ci aspettiamo di recuperare. È il controllo minimo per evitare di
considerare “migliore” una ricerca soltanto perché sembra sofisticata.
## 4. Una struttura comune, otto tipi distinti
La separazione tra tipi non viene eliminata. Ogni documento ha un involucro comune e
una parte specializzata determinata da `kind`.
### 4.1 Campi comuni
```yaml
schema_version: 1
id: evidence:fascia-pediatrica
title: Fascia pediatrica
kind: formula
purposes:
- sql_generation
- schema_linking
applies_to:
concepts:
- fascia pediatrica
tables:
- clinical.patient
columns:
- clinical.patient.birth_date
language: it
provenance:
source_file: source/10-domini-clinici/paziente.md
source_sha256: sha256:0123456789abcdef...
supporting_excerpts:
- Per fascia pediatrica si intendono i pazienti con età inferiore a 18 anni.
review_items: []
```
I campi hanno ruoli diversi:
- `kind` dice **che cosa contiene** il documento;
- `purposes` dice **in quali attività può essere utile**;
- `applies_to` dice **a quali concetti o elementi del database si riferisce**;
- `provenance` permette di risalire al testo di origine;
- `review_items` rende visibili i dubbi ancora da risolvere.
Ogni unità contiene da uno a cinque `supporting_excerpts`, ciascuno lungo al massimo
1.000 caratteri. Sono citazioni brevi che il validatore deve ritrovare nel sorgente dopo
la stessa normalizzazione meccanica. Provano la tracciabilità, non la correttezza
semantica: il revisore umano deve comunque verificare che sostengano davvero il
contenuto ristrutturato.
Ogni `review_item` contiene soltanto:
```yaml
code: ambiguous_source_statement
message: Il sorgente non chiarisce se l'età sia calcolata alla data di ricovero.
field: formula.sql # opzionale
```
Non possiede stato, autore o timestamp. Tutti i review item bloccano la pubblicazione;
il curatore corregge il documento e rimuove l'item, mentre Git conserva la storia.
### 4.2 Tipi iniziali
| `kind` | Contenuto | Esempio d'uso |
| --- | --- | --- |
| `glossary` | Definizione, sinonimi e varianti linguistiche | Capire che “ricovero” e “degenza” possono indicare lo stesso concetto |
| `domain` | Regole e vincoli del dominio | Interpretare correttamente un episodio clinico |
| `enum` | Valori ammessi e loro significato | Tradurre “dimesso” nel codice memorizzato nel DWH |
| `example` | Domanda esemplificativa e interpretazione attesa | Riconoscere una formulazione già documentata |
| `mapping` | Collegamento fra concetto e schema fisico | Individuare tabella e colonne pertinenti |
| `normalization` | Regole di normalizzazione | Uniformare codici, date o varianti testuali |
| `formula` | Espressione SQL riutilizzabile e relativi input | Calcolare la fascia pediatrica dalla data di nascita |
| `reference` | Un riferimento esterno che è esso stesso contenuto recuperabile | Proporre all'utente il link a una specifica linea guida |
Un URL che documenta un'altra Evidence appartiene alla sua `provenance`. Un URL che
deve essere recuperato come risposta autonoma è invece una Evidence `reference`.
Ogni Evidence Unit possiede un solo `kind`, scelto in base ai campi strutturati che ne
definiscono il contenuto principale. `purposes` e `applies_to` possono invece avere più
valori. Quando parti dello stesso sorgente hanno identità e regole di validazione
indipendenti, vengono prodotte unità distinte; non si duplica un'unità soltanto perché è
utile in più fasi del workflow.
La classificazione procede dai contenuti più strutturati a quelli più generali:
`formula`, `enum`, `mapping`, `normalization`, `glossary`, `domain`, `example` e
`reference`. `domain` è il tipo di ripiego per una regola del dominio che non soddisfa
uno schema più specifico; `reference` si applica soltanto quando il collegamento deve
essere restituito come contenuto autonomo.
L'identificatore non incorpora il `kind`: una riclassificazione conserva l'ID, mentre
una vera divisione semantica assegna nuovi ID alle nuove unità. Un rinominamento
univocamente riconoscibile del Source Evidence tramite hash aggiorna la provenienza e
conserva gli ID esistenti.
Un nuovo identificatore usa la forma leggibile `evidence:<slug>`, viene assegnato una
sola volta e non viene ricalcolato da titolo, percorso o hash. Le collisioni ricevono un
suffisso deterministico. Dopo la prima pubblicazione cambiare ID equivale a ritirare
l'unità esistente e crearne una nuova.
### 4.3 Dati specifici per tipo
La parte specializzata è una unione discriminata: ogni `kind` ammette e richiede campi
diversi. Alcuni esempi:
```yaml
# formula
formula:
concept: fascia pediatrica
columns:
- clinical.patient.birth_date
sql: |
CASE WHEN age < 18 THEN 'pediatrica' ELSE 'adulta' END
```
```yaml
# reference
reference:
url: https://example.org/linea-guida
label: Linea guida clinica
description: Criteri usati per classificare gli episodi.
```
```yaml
# enum
enum:
column: clinical.episode.discharge_status
values:
D: dimesso
T: trasferito
```
I tipi restano quindi sfruttabili sia in validazione sia in ricerca. Una formula non è
un semplice testo etichettato: possiede obbligatoriamente un concetto, le colonne di
input e una singola espressione PostgreSQL componibile. `SELECT`, `WITH`, DDL e DML come
statement completi non sono Formula Evidence; una query completa documentata appartiene
a `example`. Le formule legacy incompatibili diventano review item durante la migrazione.
## 5. Pre-processing dei testi sorgente
Il comando concettuale è:
```text
tht evidence prepare <workspace-root>
```
Per l'utente è una sola operazione. Internamente esegue quattro passaggi.
### 5.1 Estrazione deterministica
Il sistema:
- individua i file ammessi in `evidence/source/`;
- verifica dimensione, codifica UTF-8 e percorso sicuro;
- calcola l'hash del contenuto;
- confronta il risultato con `manifest.yaml`;
- carica, quando esiste, la precedente versione curata collegata al sorgente.
Un sorgente invariato non viene nuovamente elaborato.
Se la `pipeline_version` del manifest non è compatibile con quella installata, il
normale `prepare` termina senza scrivere. Il curatore può scegliere esplicitamente
`prepare --upgrade`, esclusivamente su un repository pulito, per rielaborare tutti i
sorgenti e revisionare il diff completo.
### 5.2 Normalizzazione deterministica
Prima del modello vengono normalizzati soltanto aspetti meccanici:
- terminatori di riga e Unicode;
- spaziatura e intestazioni palesemente riconoscibili;
- elenchi, tabelle, blocchi SQL e URL;
- metadati già esplicitamente presenti;
- riferimenti a tabelle e colonne riconoscibili.
Questa fase non interpreta il significato e non inventa strutture semantiche.
### 5.3 Una sola ristrutturazione assistita dal modello
Per ogni sorgente cambiato il modello riceve:
- il testo normalizzato;
- gli otto schemi ammessi;
- le regole “non inventare” e “segnala il dubbio”;
- le precedenti Evidence curate derivate da quel sorgente;
- gli identificatori già assegnati.
Per un'unità già esistente il modello può restituire soltanto uno degli identificatori
ricevuti. Per una nuova unità non propone l'ID: il preparatore assegna una sola volta
`evidence:<slug>` e l'eventuale suffisso deterministico. Un identificatore sconosciuto
prodotto dal modello rende la risposta non valida.
Può:
- assegnare titoli;
- classificare il tipo;
- separare un sorgente in più Evidence Unit;
- riordinare e riscrivere per chiarezza;
- compilare campi strutturati con fatti presenti nel sorgente.
- citare da uno a cinque brevi estratti del sorgente che sostengono ciascuna unità.
Non può:
- fondere automaticamente sorgenti diversi;
- aggiungere fatti non documentati;
- risolvere silenziosamente un'ambiguità;
- cancellare un'unità precedentemente revisionata.
Se il sorgente esiste ancora ma non sostiene più un'unità precedente, il modello la
restituisce come retirement candidate con il `review_item`
`source_no_longer_supports_unit`. Il curatore decide se eliminarla o riscriverla; fino a
quel momento la pubblicazione resta bloccata.
Quando una precedente unità viene realmente divisa in più unità autonome, le nuove
unità ricevono nuovi ID e la precedente rimane una retirement candidate finché il
curatore non la ritira esplicitamente.
Pi viene usato in modalità non interattiva e senza strumenti di scrittura. È un
dettaglio interno del comando, non una nuova tipologia di sessione ThothII.
Timeout, uscita non valida o JSON malformato interrompono il comando con un errore
attribuito al sorgente. La prima versione non esegue retry automatici.
### 5.4 Validazione deterministica
L'output del modello non viene scritto direttamente. Viene prima controllato:
- schema comune e schema specifico del `kind`;
- unicità e stabilità degli identificatori;
- appartenenza alle enumerazioni ammesse;
- esistenza e hash del sorgente;
- presenza nel sorgente normalizzato di ogni `supporting_excerpt`;
- correttezza sintattica di URL, tabelle, colonne e SQL dove applicabile;
- assenza di credenziali;
- coerenza tra directory e `kind`;
- assenza di collegamenti a sorgenti diversi nella stessa unità.
Esistono tre esiti.
| Esito | Comportamento |
| --- | --- |
| Valido | Il documento è pronto per la revisione Git |
| Valido con dubbi | Il documento viene scritto con `review_items`; non è indicizzabile |
| Non valido | Il documento non è pubblicabile e il rapporto spiega l'errore |
Un dubbio reale può essere mantenuto soltanto se il revisore lo trasforma in una
limitazione esplicita del contenuto e svuota `review_items`.
### 5.5 Applicazione atomica
Tutti gli output dei sorgenti cambiati vengono costruiti e validati in un'area
temporanea. Soltanto quando l'intero batch è valido, il comando sostituisce insieme i
documenti interessati e `manifest.yaml`. Un singolo errore lascia il worktree invariato
e il rapporto limitato elenca tutti i problemi rilevati. Non esiste successo parziale.
## 6. Aggiornamenti incrementali e revisione delle correzioni umane
La precedente versione curata è un input, non un file usa-e-getta. Il modello deve
proporre una modifica minima senza ricominciare da zero, ma questa istruzione non viene
presentata come una garanzia semantica. La garanzia è Git: la versione precedente resta
recuperabile, ogni variazione è visibile nel diff e nessuna proposta diventa Published
Evidence senza una nuova revisione umana.
Il comando:
- si rifiuta di operare se `evidence/curated/` o `evidence/manifest.yaml` contengono
modifiche Git non salvate;
- mantiene gli ID associati a contenuti che rappresentano ancora la stessa unità;
- riconosce come rinominato un sorgente nuovo che corrisponde univocamente all'hash di
un sorgente rimosso e ne aggiorna la provenienza senza cambiare gli ID;
- mostra come diff le variazioni proposte;
- non modifica i file derivati da sorgenti invariati;
- segnala come orfana un'unità il cui sorgente è stato rimosso;
- non elimina mai automaticamente un'unità orfana;
- blocca la pubblicazione finché ogni unità orfana non viene eliminata, ricollegata o
ricondotta a un sorgente ripristinato.
Git fornisce confronto, revisione, cronologia e recupero. Non viene introdotto un
database di authoring parallelo.
Orfani e retirement candidate vengono risolti senza modificare manualmente il manifest:
```text
tht evidence resolve <evidence-id> --retire
tht evidence resolve <evidence-id> --source <source-path>
```
Le due azioni sono mutuamente esclusive, richiedono un worktree pulito e aggiornano
atomicamente file curato e manifest. `--retire` rimuove l'unità dal corpus di authoring;
`--source` aggiorna provenienza e hash soltanto verso un sorgente esistente. Entrambe
lasciano un diff Git recuperabile, senza commit o pubblicazione automatica. Se l'unità
deve essere riscritta, il curatore modifica invece il documento e poi esegue
`validate`.
## 7. Revisione e pubblicazione
Il flusso di pubblicazione è:
```text
prepare → revisione Git → validate → merge → attivazione workspace
→ preprocess evidence candidata → evaluate candidata
→ pubblicazione atomica della generazione Qdrant
```
### 7.1 Approvazione umana
L'approvazione coincide con il normale processo Git del repository del workspace:
1. il curatore esegue `prepare` in un clone di authoring;
2. legge i documenti e il diff;
3. corregge i contenuti;
4. esegue `tht evidence validate`;
5. apre o approva la pull request;
6. esegue il merge.
La prima versione non crea automaticamente branch, commit o pull request.
### 7.2 Quando un documento diventa Published Evidence
Una Curated Evidence diventa Published Evidence soltanto quando:
- appartiene a una revisione Git pulita che ha superato il processo umano di revisione;
- la revisione è stata attivata dal registry di ThothII;
- non contiene `review_items` irrisolti;
- il manifest non contiene unità orfane;
- l'intero corpus supera la validazione;
- la Candidate Evidence Generation supera il gate top-10;
- la generazione Qdrant viene quindi pubblicata atomicamente.
Il runtime non tenta di ricostruire come sia avvenuta l'approvazione Git e il manifest
non contiene un flag `approved`. Il confine verificabile di pubblicazione è la
combinazione di revisione attiva, validazione superata e generazione Evidence attiva.
Il descriptor filesystem deve indicizzare solo `curated/**/*.md`. I sorgenti e i file
di supporto restano materializzati per tracciabilità, ma non entrano nell'indice.
## 8. Indicizzazione e generazioni
L'indicizzazione continua a usare il meccanismo già implementato dal modulo Evidence:
1. legge la radice materializzata della revisione Git attiva;
2. valida nuovamente tutte le Evidence;
3. costruisce Evidence Fragment secondo sezioni semantiche;
4. verifica il vettore dense predefinito già usato da Schema e Memory;
5. aggiunge in modo non distruttivo il vettore sparse `bm25` se manca;
6. genera le rappresentazioni dense degli Evidence Fragment;
7. chiede a Qdrant di generare per gli stessi frammenti la rappresentazione lessicale
BM25;
8. carica i punti con la nuova `vector_generation`;
9. verifica manifest, conteggi e leggibilità;
10. rende attiva la nuova generazione;
11. conserva le generazioni precedenti previste dalla policy.
Se uno dei passaggi fallisce, la generazione precedente rimane attiva. I punti caricati
parzialmente vengono compensati secondo il meccanismo transazionale già esistente.
L'eventuale configurazione `bm25` già aggiunta rimane: è compatibile con i punti dense
esistenti e non richiede rollback. Se `bm25` esiste con una configurazione diversa da
`modifier: idf`, la procedura fallisce senza modificarla.
L'upgrade non ricrea la collezione. I record `schema_table`, `schema_column`, `memory` e
`solved_question` restano invariati e continuano a usare il vettore dense predefinito.
Soltanto gli Evidence Fragment ricevono anche `bm25`. Durante la finestra fra aggiunta
del vettore e pubblicazione della prima candidata ibrida, Evidence è `unavailable`, ma
Schema e Memory continuano a funzionare.
## 9. Qdrant spiegato senza presupporre conoscenze vettoriali
### 9.1 L'analogia della biblioteca
Si può immaginare Qdrant come il catalogo di una biblioteca.
- Le **Evidence Unit** sono i documenti completi conservati negli scaffali Git.
- Gli **Evidence Fragment** sono le schede del catalogo relative alle singole sezioni.
- I **vettori** sono rappresentazioni numeriche usate per confrontare una domanda con
quelle schede.
- Il **payload** è l'insieme delle etichette leggibili: tipo, scopo, tabelle, colonne,
revisione e documento di origine.
Qdrant non decide se una Evidence è vera e non sostituisce il documento. Aiuta soltanto
a trovare rapidamente le schede più promettenti.
### 9.2 Ricerca per significato: vettore dense
La rappresentazione dense descrive il significato generale di una frase. Permette, per
esempio, di avvicinare “pazienti minorenni” a “fascia pediatrica” anche quando le parole
non coincidono.
È utile per il linguaggio naturale, ma può essere meno precisa con codici, acronimi,
nomi di colonne e formule.
### 9.3 Ricerca per parole e identificatori: BM25 sparse
La rappresentazione sparse conserva il peso delle parole presenti. È adatta a termini
come `ICD-10`, `discharge_status`, `ADT`, un valore enum o un nome esatto di colonna.
Qdrant 1.18.2 può generare questa rappresentazione direttamente sul server usando
`qdrant/bm25`; per il corpus italiano si passa `language: italian` sia durante il
caricamento sia durante la ricerca. Non serve aggiungere FastEmbed o un nuovo servizio.
Un test L0 avvia esattamente l'immagine Qdrant dichiarata da `compose.yaml`, crea una
collezione temporanea, indicizza due testi italiani mediante `qdrant/bm25`, verifica una
ricerca lessicale e infine elimina la collezione. Questo rende controllabile la capacità
locale richiesta prima di qualunque migrazione reale. Se la prova fallisce non esiste
un fallback silenzioso a un motore diverso.
Il nome “sparse” significa soltanto che, tra moltissime parole possibili, ogni testo ne
usa poche. Qdrant mantiene anche l'IDF: una parola rara pesa più di una parola presente
quasi ovunque.
### 9.4 Perché combinarle
Una domanda può richiedere contemporaneamente comprensione e precisione lessicale:
> “Qual è la formula per distinguere la fascia pediatrica usando
> `patient.birth_date`?”
La ricerca dense riconosce il concetto; BM25 riconosce con forza “formula” e il nome
della colonna. Qdrant esegue entrambe e produce due graduatorie.
### 9.5 Reciprocal Rank Fusion
Reciprocal Rank Fusion, o RRF, combina le due graduatorie usando la posizione dei
risultati invece di confrontare direttamente punteggi di natura diversa.
In termini pratici:
- un documento alto in entrambe le liste sale;
- un documento molto forte in una sola lista può comunque emergere;
- non occorre inventare una conversione fragile fra “similarità semantica” e “punteggio
delle parole”.
Si parte con i pesi predefiniti. Pesi diversi saranno introdotti soltanto se
`evaluation.yaml` dimostrerà un miglioramento.
### 9.6 Il ruolo dei metadati
Ogni punto Qdrant conserva almeno:
```text
workspace_id
workspace_revision
vector_generation
record_kind
evidence_id
evidence_kind
purposes
concepts
tables
columns
language
source_file
source_sha256
fragment_ordinal
```
I metadati hanno due usi:
- workspace, revisione, generazione e purpose sono filtri obbligatori;
- tipo, concetti, tabelle e colonne diventano filtri soltanto quando il chiamante li
dichiara vincolanti; altrimenti contribuiscono al testo della query e alla spiegazione
del risultato.
La prima versione non aggiunge bonus automatici per `kind` o `applies_to`. Durante la
generazione SQL una Formula Evidence viene imposta soltanto quando il workflow richiede
esplicitamente `kind=formula`; negli altri casi dense, BM25 e RRF determinano l'ordine.
Quando concetti, tabelle o colonne non sono vincoli, il modulo costruisce un solo testo
deterministico, identico per dense e BM25:
```text
Domanda: <domanda originale>
Concetti: <valori deduplicati e ordinati>
Tabelle: <valori deduplicati e ordinati>
Colonne: <valori deduplicati e ordinati>
```
Le righe vuote sono omesse. La domanda conserva formulazione e ordine originali; il
renderer applica soltanto Unicode NFC, converte CRLF e CR in `\n`, rimuove gli spazi
esterni e rifiuta una domanda vuota. Non cambia maiuscole, punteggiatura o spazi interni.
Ai valori contestuali applica NFC e `strip`, elimina stringhe vuote e duplicati esatti e
li ordina per valore Unicode senza `lower()` o `casefold()`: gli identificatori
PostgreSQL quotati possono essere sensibili alle maiuscole. In questo modo due richieste
equivalenti non cambiano per effetto dell'ordine occasionale dei metadati.
### 9.7 Perché non creare una collezione per tipo
Una domanda spesso attraversa più tipi: una formula può dipendere da un mapping, da un
enum e da una regola di dominio. Collezioni separate richiederebbero più interrogazioni,
fusione applicativa e più operazioni di manutenzione.
La soluzione usa la collezione semantica già posseduta dal workspace, conserva il suo
vettore dense predefinito e aggiunge soltanto il vettore sparse denominato `bm25`. I
payload indicizzati distinguono i tipi. È più semplice, evita di ricostruire Schema e
Memory e permette a Qdrant di eseguire ricerca ibrida e filtri nella stessa Query API.
### 9.8 Perché indicizzare frammenti ma restituire unità
Un documento lungo può contenere sezioni diverse. Un unico vettore ne diluirebbe il
significato; frammenti arbitrari di lunghezza fissa spezzerebbero invece formule o
regole.
La divisione segue intestazioni, campi tipizzati e confini di paragrafo. Non divide mai
una formula, una coppia valore/significato, un mapping, una regola o un URL. Se uno di
questi elementi atomici supera da solo `max_chunk_chars`, la preparazione aggiunge il
Review item stabile `atomic_content_too_large` e blocca la pubblicazione: non usa un
taglio a dimensione fissa che ne altererebbe il significato. Il limite è quello già
presente nella configurazione degli embeddings, pari per default a 4.000 caratteri, e
si applica all'intero testo reso che sarà inviato all'embedder, incluse etichette e
metadati testuali. Non viene introdotta una seconda impostazione. Qdrant trova i
frammenti, poi l'Evidence Module li raggruppa per `evidence_id` e restituisce un solo
Evidence Result con i migliori estratti, provenienza, citazione e riferimento al
documento completo. Il contenuto completo viene risolto soltanto quando il workflow ne
ha bisogno.
### 9.9 Cosa non introduciamo nella prima versione
- una collezione per ogni tipo;
- ColBERT o multivettori late-interaction;
- un reranker basato su un altro modello;
- pesi RRF regolati a mano senza misurazioni;
- un servizio separato per BM25;
- ricerca automatica sul web.
Queste possibilità rimangono future ottimizzazioni, non prerequisiti.
Riferimenti tecnici ufficiali:
- [Qdrant: Text Search](https://qdrant.tech/documentation/search/text-search/)
- [Qdrant: server-side BM25](https://qdrant.tech/documentation/inference/inference-bm25/)
- [Qdrant: Hybrid Queries e RRF](https://qdrant.tech/documentation/search/hybrid-queries/)
- [Qdrant: aggiornamento dello schema dei vettori](https://qdrant.tech/documentation/manage-data/collections/#update-vector-schema)
- [Qdrant: payload indexing](https://qdrant.tech/documentation/manage-data/indexing/)
- [Qdrant: multitenancy](https://qdrant.tech/documentation/manage-data/multitenancy/)
## 10. Contratto di ricerca del modulo Evidence
Il workflow non costruisce query Qdrant. Usa una sola interfaccia concettuale:
```python
search(
query: str,
purpose: EvidencePurpose,
context: EvidenceSearchContext,
) -> EvidenceSearchOutcome
```
`EvidenceSearchContext` può specificare tabelle, colonne e concetti da aggiungere alla
query, oltre a vincoli espliciti su tipo, tabelle, colonne o concetti.
Il modulo rende domanda e contesto una sola volta nel formato `Domanda`, `Concetti`,
`Tabelle`, `Colonne` definito sopra e passa esattamente quel testo sia all'embedder dense
sia a `qdrant/bm25`.
`EvidenceSearchOutcome` distingue due stati:
- `available`, con la generazione interrogata e zero o più `EvidenceResult`;
- `unavailable`, senza risultati e con un codice di errore stabile e un messaggio
limitato.
Una lista vuota nello stato `available` significa che la ricerca ha funzionato ma non
ha trovato corrispondenze. Non equivale a un errore tecnico.
Il modulo Evidence possiede interamente:
- generazione della query dense;
- query BM25 con lingua coerente;
- filtri su workspace, revisione, generazione e purpose;
- RRF;
- vincoli espliciti su `kind` e `applies_to`;
- raggruppamento dei frammenti;
- risoluzione di provenienza e citazioni;
- controllo della revisione attiva.
Il workflow riceve candidati spiegabili, mai verità automatiche.
## 11. Inserimento nel workflow modulare ThothII
Evidence rimane un modulo autonomo con due responsabilità pubbliche.
### 11.1 Authoring
```text
prepare → validate → evaluate
```
Questa superficie è usata dal curatore e non dalle sessioni.
### 11.2 Runtime
```text
search → resolve citation → project into session
```
Evidence è un contributore degli stage esistenti, non uno stage aggiuntivo. Non emette
decisioni, non scrive gli artifact canonici e non modifica il ledger o lo stato del
workflow. Lo stage chiamante decide come usare i candidati restituiti.
L'integrazione usa l'identità semantica dello stage, non il display code:
| Stage semantico | Display code attuale | Evidence purpose |
|---|---:|---|
| `clarification` | F1 | `disambiguation` |
| `rewriting` | F3 | `rewriting` |
| `schema_linking` | F4 | `schema_linking` |
| `cte` | F6 | `sql_generation` |
| `final_sql` | F7 | `sql_generation` |
Lo stage `memory` (F2) usa il Memory Module. Lo stage `synthesis` (F5) verifica e
riassume lo schema linking già approvato e non avvia una nuova ricerca Evidence.
Ogni stage elencato esegue una ricerca indipendente con gli input disponibili in quel
momento. In particolare `cte` usa domanda riscritta e schema approvato, mentre
`final_sql` aggiunge il piano CTE approvato. La prima versione non introduce una cache
condivisa fra stage.
Il chiamante conserva nella sessione una Evidence receipt con stage, purpose,
generazione e ID restituiti. Il testo non viene copiato: rimane nel repository del
workspace e viene risolto attraverso la provenienza della Published Evidence.
Gli stage passano `purpose` e contesto al modulo, ma non conoscono collezioni, nomi di
vettori, generazioni o sintassi Qdrant.
Le istruzioni Pi relative alla consultazione delle Evidence vengono spostate in
frammenti del modulo Evidence e poi proiettate nel `SKILL.md` generato, seguendo il
meccanismo modulare già usato da Disambiguation e Memory.
## 12. Formule
Le formule approvate oggi presenti nello store `formulas/*.sql.md` vengono convertite in
Evidence `kind: formula`. Dopo la migrazione non esistono due archivi runtime.
Una nuova formula scoperta in F4 segue invece questo percorso:
```text
sessione → Formula proposal nell'artefatto di sessione
→ importazione di manutenzione
→ Curated Evidence formula
→ revisione Git
→ Published Evidence
```
Le decisioni `concept_formula_approved` e `concept_formula_rejected` continuano a
descrivere la scelta fatta nella singola sessione. Non equivalgono alla pubblicazione
globale nel workspace.
## 13. Comportamento in caso di errore
### 13.1 Durante l'authoring
- un file non UTF-8, troppo grande o strutturalmente invalido produce un errore chiaro;
- un dubbio semantico produce un `review_item`;
- un albero Git sporco impedisce la scrittura di nuove proposte;
- un sorgente rimosso produce un'unità orfana, non una cancellazione, e blocca la
pubblicazione finché il curatore non la risolve.
### 13.2 Durante l'indicizzazione
- la nuova generazione viene preparata senza toccare quella attiva;
- la generazione candidata viene interrogata esplicitamente per la valutazione senza
renderla visibile alle sessioni;
- una valutazione fallita lascia inattiva la candidata;
- un caricamento o una verifica falliti non cambiano il puntatore attivo;
- i dati parziali vengono rimossi quando possibile e comunque non sono leggibili dal
runtime perché manca l'attivazione.
Poiché la revisione del workspace viene attivata prima di costruire la candidata, il
runtime può attraversare una finestra di manutenzione in cui la vecchia generazione non
corrisponde alla revisione. In questa finestra la ricerca Evidence è `unavailable` e
blocca lo stage chiamante. La prima versione accetta questa degradazione fail-closed
invece di introdurre una transazione distribuita fra Git, registry e Qdrant. Se il gate
fallisce, l'operatore corregge il corpus oppure ripristina esplicitamente la revisione
precedente.
### 13.3 Durante una sessione
Una ricerca `available` senza corrispondenze produce una Evidence receipt vuota, viene
mostrata come tale e non impedisce allo stage di continuare.
Se Qdrant, il corpus attivo o la revisione attesa non sono disponibili:
- l'outcome è `unavailable`, non una lista vuota valida;
- viene restituito un codice stabile con un messaggio limitato;
- lo stage chiamante resta bloccato e può essere ritentato;
- non vengono usate revisioni precedenti;
- non viene ripetuta la ricerca con un purpose diverso.
Questo comportamento è fail-closed: un'assenza reale di corrispondenze non ferma il
workflow, mentre un guasto non viene mascherato come assenza di conoscenza.
## 14. Valutazione minima
`tht evidence evaluate` esegue le domande in `evaluation.yaml` contro una generazione
indicata esplicitamente oppure, per il monitoraggio ordinario, contro l'indice attivo.
Riporta almeno:
- quante domande hanno trovato una Evidence attesa nei primi 5 e nei primi 10 risultati;
- quali tipi attesi sono mancati;
- quali query non hanno prodotto risultati;
- per ogni Evidence attesa, la posizione nella graduatoria dense, BM25 e fused;
- revisione Git, generazione e configurazione di ricerca usate.
Il file classifica ogni domanda come `lexical`, `semantic` o `mixed` e contiene almeno
un caso per profilo. L'evaluator esegue i due rami anche separatamente per renderli
diagnosticabili, oltre alla ricerca ibrida usata dal runtime. La prima baseline deve
essere salvata prima di regolare pesi o introdurre altri modelli. Il comando non
modifica l'indice. La valutazione supera il gate minimo soltanto quando ogni query trova
almeno una delle Evidence attese nei primi dieci risultati fused. Le posizioni dei
singoli rami e `hit@5` restano informative e non bloccano la pubblicazione.
## 15. Comandi e responsabilità
| Comando | Dove opera | Scrive |
| --- | --- | --- |
| `tht evidence prepare <workspace-root> [--upgrade]` | clone Git di authoring | `curated/`, `manifest.yaml` |
| `tht evidence validate <workspace-root>` | clone Git o CI | nulla |
| `tht evidence resolve <id> (--retire | --source <path>)` | clone Git di authoring | unità interessata, `manifest.yaml` |
| `tht evidence evaluate ... [--generation <id>]` | generazione candidata o attiva | solo rapporto su stdout/JSON |
| `tht ... workspace preprocess evidence` | installazione/runtime | generazione candidata, poi attiva soltanto dopo il gate |
`prepare` non crea commit. `preprocess evidence` non modifica il repository Git.
## 16. Migrazione iniziale del workspace PSD
Il corpus attuale comprende 36 file Markdown organizzati in glossario, domini clinici,
enum, esempi NLQ, mapping e normalizzazione. La migrazione avviene così:
1. spostare gli originali sotto `evidence/source/`, conservandone la gerarchia;
2. eseguire `prepare` e generare `curated/`;
3. revisionare tutte le unità e risolvere i `review_items`;
4. importare eventuali formule approvate come `kind: formula`;
5. compilare circa venti query in `evaluation.yaml`;
6. configurare il descriptor con `patterns: ["curated/**/*.md"]`;
7. validare, fare merge e attivare la revisione in una finestra di manutenzione;
8. registrare conteggi e ID campione di Schema e Memory;
9. eseguire `preprocess evidence`, che aggiunge `bm25` senza ricreare la collezione e
costruisce la generazione candidata;
10. verificare che conteggi, ID campione e ricerche dense di Schema e Memory siano
invariati;
11. valutare la candidata e pubblicarla soltanto se supera il gate top-10;
12. salvare la baseline e svolgere una verifica umana degli stage `clarification`,
`rewriting`, `schema_linking`, `cte` e `final_sql`.
Non serve mantenere v1 e v2 attivi contemporaneamente nel runtime: Git conserva la
vecchia revisione e il meccanismo delle generazioni conserva il rollback dell'indice.
## 17. Criteri di accettazione
La prima versione è completa quando:
1. un sorgente poco strutturato produce una o più unità tipizzate senza perdere la
provenienza;
2. sorgenti invariati sono un no-op;
3. ogni modifica proposta a contenuti revisionati è recuperabile e visibile nel diff
Git prima della pubblicazione;
4. ogni unità cita brevi estratti verificabili del proprio sorgente;
5. gli ID nuovi sono assegnati dal codice e il modello può soltanto riutilizzare ID
precedenti esplicitamente forniti;
6. il batch di preparazione è tutto-o-niente e non esegue retry automatici;
7. un `review_item` impedisce l'indicizzazione;
8. un'unità orfana blocca la pubblicazione finché non viene risolta;
9. un'unità non più sostenuta dal proprio sorgente diventa una retirement candidate e
blocca la pubblicazione;
10. il comando `resolve` ritira o ricollega un'unità con un diff Git recuperabile;
11. tutte le otto varianti hanno validazione specifica e un solo `kind` primario;
12. una riclassificazione conserva l'ID e un rinominamento univoco del sorgente conserva
gli ID delle unità collegate;
13. ogni ID usa `evidence:<slug>`, non viene ricalcolato automaticamente e non contiene
il kind;
14. ogni Formula Evidence contiene una sola espressione PostgreSQL componibile;
15. le formule approvate sono ricercate tramite lo stesso modulo delle altre Evidence;
16. soltanto `curated/**/*.md` entra nel corpus runtime;
17. la collezione Qdrant conserva il dense predefinito e aggiunge `bm25` con IDF senza
ricostruzione distruttiva;
18. la Query API esegue i due prefetch e la fusione RRF;
19. risultati di frammenti della stessa unità diventano un solo Evidence Result con
estratti e riferimento al documento completo;
20. una ricerca disponibile può restituire zero Evidence, mentre una revisione o
generazione non corrispondente produce `unavailable` e blocca lo stage;
21. ogni ricerca applica il purpose come filtro obbligatorio e non usa bonus impliciti
per kind o ambito;
22. la generazione candidata diventa attiva soltanto quando ogni query recupera almeno
una Evidence attesa nei primi dieci risultati; il rapporto include anche `hit@5`;
23. i cinque stage mappati interrogano Evidence indipendentemente, mentre `memory` e
`synthesis` non lo invocano;
24. ogni ricerca disponibile conserva una ricevuta minima senza duplicare il testo;
25. una sessione completa riprende dopo la finestra fail-closed mediante retry o
rollback esplicito;
26. conteggi, ID campione e ricerche dense dimostrano che Schema e Memory non cambiano
durante l'upgrade;
27. una prova L0 dimostra `qdrant/bm25` sull'immagine locale effettivamente dichiarata;
28. dense e BM25 ricevono lo stesso testo di query deterministico;
29. nessun elemento atomico viene spezzato per rispettare la dimensione dei frammenti;
30. la valutazione copre casi lessicali, semantici e misti e mostra separatamente i tre
ranking;
31. il testo reso di ogni frammento rispetta il solo `max_chunk_chars` esistente;
32. la query conserva maiuscole, punteggiatura e spazi interni e non altera gli
identificatori PostgreSQL sensibili alle maiuscole;
33. documentazione e comandi descrivono lo stesso contratto.
## 18. Decisioni rinviate
Saranno considerate soltanto dopo la baseline:
- pesi RRF diversi da quelli predefiniti;
- reranking;
- ColBERT o multivettori;
- acquisizione automatica di PDF, Word, HTML o pagine web;
- creazione automatica di branch e pull request;
- fusione assistita di Evidence provenienti da sorgenti diversi.
Queste esclusioni mantengono la prima implementazione comprensibile, realizzabile,
manutenibile e documentabile.