# Gestione delle relationship: da ThothAI a ThothII **Stato:** analisi e direzione funzionale/UX confermate nel *grill with docs*; non costituisce ancora un piano di implementazione. **Revisione esaminata:** commit ThothII `f586152636b1fd653b0bc1d40b54be7f89bbd2bb`. I sorgenti legacy di ThothAI citati sotto sono versionati nello stesso repository, sotto `Thoth/ThothAI/`. **Ambito:** import delle foreign key fisiche, inferenza di relationship logiche, modifica umana, pubblicazione verso il workflow NL→SQL e principali gap tra i due sistemi. ## Sintesi fattuale 1. In ThothAI le foreign key dichiarate nel database venivano importate e una procedura separata proponeva relationship basate sul nome dei campi e su una verifica dei valori. Entrambi i percorsi scrivevano però nello stesso modello `Relationship` (`Thoth/ThothAI/backend/thoth_core/dbmanagement.py:484-640`; `Thoth/ThothAI/backend/thoth_core/admin_models/admin_sqldb.py:571-826`; `Thoth/ThothAI/backend/thoth_core/models.py:488-530`). 2. Il ricordo di una relationship inferita persistita come `generated` non trova riscontro nel modello esaminato: `Relationship` contiene solo quattro foreign key verso tabelle e colonne, senza provenienza, stato, confidenza o flag `generated`. La procedura di inferenza calcola localmente pattern e tasso di validazione, ma non li salva (`Thoth/ThothAI/backend/thoth_core/models.py:488-530`; `Thoth/ThothAI/backend/thoth_core/admin_models/admin_sqldb.py:697-801`). 3. L'amministratore Django di ThothAI permetteva CRUD manuale sul medesimo insieme di relationship e verificava che gli endpoint appartenessero allo stesso database e alle tabelle selezionate; non distingueva visivamente o semanticamente relationship fisiche, inferite e manuali (`Thoth/ThothAI/backend/thoth_core/admin_models/admin_relationship.py:19-313`). 4. ThothII oggi separa già i due concetti: il catalogo backend conserva solo le foreign key fisiche dichiarate, mentre il core/harness possiede annotazioni di foreign key logiche curate e un comando che suggerisce candidate per nome, primary key e SQL osservato (`CONTEXT.md:291-300`; `docs/adr/0006-separate-physical-and-logical-relationships.md:3-8`; `harness/tht/cli/schema_cmd.py:203-299`). 5. I due mondi ThothII non sono ancora integrati: i record di Database Management non modificano il workflow NL→SQL e l'integrazione catalogo→core/schema-linking è ancora un gate di design esplicitamente differito (`PROJECT_STATE.md:116-124`; `PROJECT_STATE.md:161-165`). ## Evidenze ThothAI ### Foreign key ufficiali Il percorso di import legge le foreign key dal database, limita l'import alle tabelle già presenti nel catalogo, crea le colonne mancanti, crea o riusa un record `Relationship` e aggiorna anche le stringhe denormalizzate `pk_field`/`fk_field` sulle colonne (`Thoth/ThothAI/backend/thoth_core/dbmanagement.py:484-640`, in particolare `:501-513`, `:526-591` e `:606-607`). **Fatto:** una foreign key fisica diventa quindi un record applicativo, non rimane soltanto un fatto letto al momento dal database. ### Relationship inferite La routine legacy dichiara sei famiglie di confronto tra il nome della colonna candidata e quello della primary key: corrispondenza esatta, snake case, kebab case, camel case, concatenazione e solo nome tabella (`Thoth/ThothAI/backend/thoth_core/admin_models/admin_sqldb.py:571-689`). Per una candidata, la routine: - legge fino a 20 valori distinti e non nulli dalla colonna candidata; - verifica ogni valore contro la primary key bersaglio; - accetta la candidata quando almeno il 70% dei valori esaminati trova riscontro; - crea o recupera un normale `Relationship` e aggiorna i campi denormalizzati delle tabelle (`Thoth/ThothAI/backend/thoth_core/admin_models/admin_sqldb.py:697-801`). **Correzione documentale:** un commento parla di campionamento casuale, ma la query mostrata non contiene un ordinamento casuale; il comportamento verificabile dal codice è “fino a 20 valori distinti e non nulli”, non un campione statisticamente casuale (`Thoth/ThothAI/backend/thoth_core/admin_models/admin_sqldb.py:697-716`). **Rischio legacy:** nomi di tabelle e colonne sono interpolati direttamente in SQL in questo percorso. Portare la logica letteralmente in ThothII riprodurrebbe un problema di quoting/sicurezza e richiederebbe inoltre una policy esplicita per l'accesso ai valori del DWH (`Thoth/ThothAI/backend/thoth_core/admin_models/admin_sqldb.py:709-750`). ### Un solo modello per tre origini Il modello `Relationship` legacy contiene soltanto `source_table`, `target_table`, `source_column` e `target_column`, più metodi di rappresentazione/aggiornamento. Non contiene campi per origine, algoritmo, evidenza, confidenza, approvazione o disabilitazione, né un vincolo di unicità dichiarato nel modello (`Thoth/ThothAI/backend/thoth_core/models.py:488-530`). L'admin consente aggiunta, modifica e cancellazione ordinarie e valida la coerenza tra database, tabelle e colonne (`Thoth/ThothAI/backend/thoth_core/admin_models/admin_relationship.py:30-38`, `:125-157`, `:218-245`). **Conclusione fattuale:** relationship importate, inferite e create manualmente convergono nello stesso tipo persistito. Dal record finale non è possibile ricostruirne con certezza l'origine. Il termine `generated`, se usato nell'interfaccia o nel linguaggio operativo dell'epoca, non era una qualificazione persistita dal modello esaminato. ### Uso nella comprensione dello schema La generazione M-Schema conserva colonne PK/FK e ricostruisce la sezione `【Foreign keys】` analizzando le stringhe denormalizzate `fk_field` (`Thoth/ThothAI/frontend/sql_generator/helpers/main_helpers/main_generate_mschema.py:25-61`, `:94-101`, `:160-187`). **Fatto:** le relationship curate nel catalogo legacy influenzavano la rappresentazione dello schema consumata dal generatore SQL, anche se tramite una proiezione denormalizzata. ## Stato attuale di ThothII ### Catalogo backend: relationship fisiche Il modello di dominio corrente distingue esplicitamente: - **Physical Relationship:** vincolo dichiarato nel database; - **Catalog Relationship:** copia persistita di quel fatto fisico, non creabile o modificabile manualmente ma eliminabile per cleanup; - **Logical Relationship:** relazione semantica curata o inferita, con ownership e lifecycle separati (`CONTEXT.md:291-300`; `docs/adr/0006-separate-physical-and-logical-relationships.md:3-8`). La migrazione del catalogo crea `catalog_relationships` e le coppie ordinate in `catalog_relationship_columns`. L'identità della relazione fisica è `(source_table_id, constraint_name)`; non esistono campi di origine, stato o confidenza (`backend/src/catalog/migrations/003_catalog_schema_sync.ts:42-74`). Il tipo TypeScript è solo strutturale e le descrizioni generate riguardano esclusivamente tabelle o colonne (`backend/src/catalog/types.ts:123-152`). L'introspezione PostgreSQL legge soltanto `pg_constraint` con `contype = 'f'` e mantiene l'ordine delle coppie per le chiavi composite (`backend/src/catalog/schema-introspector.ts:110-171`; `backend/src/catalog/schema-introspector.ts:360-416`; `docs/contracts/catalog-schema-snapshot.md:40-53`). La sincronizzazione autoritativa crea, aggiorna o elimina le copie fisiche in base allo snapshot (`backend/src/catalog/repository.ts:1165-1218`; `backend/src/catalog/repository.ts:1316-1393`). Le API e la UI espongono lettura, sync e cleanup in massa, non CRUD logico per singola relationship (`backend/src/routes/catalog-schema.ts:25-37`; `backend/src/routes/catalog-schema.ts:134-150`; `frontend/src/api/catalog-databases.ts:452-489`; `frontend/src/shell/database-management/DatabaseRelationships.tsx:114-123`; `frontend/src/shell/database-management/DatabaseRelationships.tsx:160-225`). **Effetto rilevante:** il cleanup è intenzionalmente reversibile tramite una sync successiva e può lasciare temporaneamente un catalogo incompleto (`docs/adr/0008-allow-manual-catalog-metadata-cleanup.md:7-16`). Un test di integrazione ammette anche una relationship rimasta senza coppie di colonne dopo la cancellazione delle colonne catalogate (`backend/test/catalog-repository.integration.test.ts:194-264`, in particolare `:246-252`). Un futuro consumer non può quindi assumere che ogni stato intermedio del catalogo sia pubblicabile così com'è. ### Core/harness: relationship logiche e suggerimenti Il modello M-Schema del core possiede già `TableAnnotation.foreign_keys`, descritte come foreign key logiche curate e unite alle foreign key fisiche (`harness/tht/mschema/models.py:35-39`; `harness/tht/mschema/models.py:76-84`). Il renderer fonde i due insiemi e li presenta insieme nella sezione `【Foreign keys】` (`harness/tht/mschema/render.py:9-20`; `harness/tht/mschema/render.py:46-80`). Il comando `schema suggest-fks` costruisce candidate da: - uguaglianze trovate in SQL, quando esattamente un lato è una primary key; - convenzione speciale `*time_key → dim_time.`; - stesso nome tra colonna e primary key a proprietario univoco; - assunzioni esplicite per disambiguare; - esclusione di nomi PK generici come `id`, `key` e `code` e dei proprietari ambigui (`harness/tht/cli/schema_cmd.py:203-299`; `harness/tht/mschema/fkmine.py:1-58`). Il comando può produrre un documento candidato e, con `--write`, aggiungere annotazioni mancanti in modo idempotente; il messaggio stesso chiede revisione manuale (`harness/tht/cli/schema_cmd.py:406-429`; `harness/tht/cli/schema_cmd.py:519-540`; `harness/tests/test_schema_fk_annotations.py:131-146`; `harness/tests/test_schema_fk_annotations.py:454-470`). **Fatto:** ThothII non parte da zero sull'inferenza. Possiede già un motore più conservativo, basato su PK univoche e SQL osservato, ma non conserva per ogni relazione origine, evidenza, frequenza o confidenza. Il miner conta le occorrenze internamente, ma il modello candidato non promuove quel conteggio a lifecycle persistito (`harness/tht/mschema/fkmine.py:1-58`; `harness/tht/mschema/models.py:35-39`). **Rischi strutturali già visibili:** - `ForeignKey` ammette liste di colonne, ma non valida che source e target abbiano la stessa cardinalità; il renderer usa `zip`, quindi una relazione malformata può essere troncata silenziosamente (`harness/tht/mschema/models.py:35-39`; `harness/tht/mschema/render.py:76-78`). - il merge evita duplicati rispetto alle FK fisiche iniziali, ma non aggiorna l'insieme `seen` dopo aver aggiunto un'annotazione; due annotazioni logiche uguali possono sopravvivere al merge (`harness/tht/mschema/render.py:9-20`). - il catalogo fisico supporta coppie composite ordinate, mentre le euristiche correnti e legacy sono sostanzialmente unary. La semantica delle candidate composite resta da decidere (`backend/src/catalog/migrations/003_catalog_schema_sync.ts:56-74`; `harness/tht/cli/schema_cmd.py:203-299`). ### Revisione e pubblicazione correnti Le annotazioni canoniche del workspace sono un blob Git. Il flusso pubblico produce candidate, verifica le annotazioni e richiede un'accettazione umana esplicita dopo commit/push/pull; l'accettazione registra digest del candidato e delle annotazioni, revisione e blob (`docs/contracts/workspace-preprocessing-cli.md:89-106`; `backend/src/workspaces/preprocessing-service.ts:202-297`). Il preprocessing successivo procede solo se il digest accettato coincide con le annotazioni correnti (`backend/src/workspaces/preprocessing-service.ts:335-388`). **Limite fattuale:** il record di review conserva digest, revisione e blob, ma non attore, motivazione o decisioni per singola candidata (`backend/src/workspaces/preprocessing-state.ts:67-73`). Il runtime usa le annotazioni quando renderizza lo schema, ma la ricerca vettoriale indicizza record di tabelle e colonne senza contenuto esplicito delle relationship (`harness/tht/vectorstore/records.py:106-138`; `harness/tht/cli/search_cmd.py:233-273`). Inoltre la vista colonne usata in F4 continua a leggere i commenti fisici, non le annotazioni (`harness/tht/cli/schema_cmd.py:592-621`). La review dei join durante una sessione è distinta dalla curatela globale: il reviewer conferma l'insieme dei join oppure chiede una revisione completa; non modifica la mappa canonica delle relationship (`harness/.pi/skills/tht-sessione/SKILL.md:301-341`; `frontend/src/widgets/JoinReviewWidget.tsx:5-84`). Il modello di sessione registra join come due stringhe e una decisione opzionale, senza ID stabile della relationship o coppie ordinate strutturate (`harness/tht/session/models.py:147-170`). ## Delta e rischi da sottoporre al grill | Tema | Fatto documentato | Delta/rischio aperto | |---|---|---| | Origine | ThothAI perdeva l'origine; ThothII separa fisico e logico a livello concettuale | Decidere quale provenienza debba essere persistita per manuale, euristica, SQL osservato e import fisico | | `generated` | Non era un flag del modello ThothAI; in ThothII “Generated Description” è già un termine del catalogo (`CONTEXT.md:302-311`) | Usare `generated` anche per relationship potrebbe creare ambiguità terminologica | | Cancellazione utente | In ThothAI era CRUD sul record unico; in ThothII il cleanup fisico viene ricostruito dalla sync | “Eliminare” può significare cancellare una relazione logica, sopprimere un fatto fisico per il core oppure pulire temporaneamente la copia catalogata: sono operazioni diverse | | Inferenza | ThothAI usava sei pattern e valori DWH; ThothII usa PK univoche, nomi e SQL osservato | Stabilire se sostituire, integrare o non portare il campionamento dei valori; servono policy di dati sensibili, query read-only, quoting e limiti | | Approvazione | ThothII dispone di review Git/digest dell'intero artefatto | Mancano decisioni per candidata, motivazione, attore, sticky rejection, versione algoritmo e gestione dello stale | | Compositi | Il catalogo fisico conserva coppie ordinate; le annotazioni accettano liste | Mancano invarianti forti e una strategia di inferenza/review per join compositi | | Pubblicazione | Catalogo management e runtime core sono oggi separati | Va stabilito se pubblicare tutto, una selezione esplicita o una revisione immutabile; il catalogo può essere incompleto durante cleanup/sync | | UI e ownership | UI catalogo fisico read-only; curatela logica via CLI/Git; review join per sessione separata | Va scelto chi cura la mappa e in quale superficie, senza confondere amministrazione globale e correzione della singola sessione | | Retrieval | Le relationship entrano nel render M-Schema ma non nei record vettoriali | Va deciso se e come influenzano selezione tabelle, ranking e descrizioni, oltre al rendering finale | | Drift | Sync fisica è autoritativa; annotazioni sono una revisione separata | Servono semantiche per endpoint rinominati/eliminati, candidate stale, orphan e riapprovazione | Nota storica: questa analisi precede il passaggio PostgreSQL catalog-to-core. Per il contratto implementato usare [architettura corrente](../architecture/overview.md) e ADR 0016; le vecchie ipotesi di pubblicazione non sono istruzioni operative. ## Direzione confermata nel grill Il 31 agosto 2026 è stato concordato il seguente flusso minimo: 1. Le foreign key dichiarate continuano a essere sincronizzate come Catalog Relationship fisiche. 2. Le foreign key ipotetiche sono salvate una sola volta come Logical Relationship fra colonna sorgente e colonna destinazione; le tabelle sono ricavate dalle colonne e la UI le presenta nel relativo contesto tabella. 3. Una relationship inferita è marcata `generated`; una relationship aggiunta dall'utente non lo è. 4. La cancellazione logica conserva il record e impedisce a una ricostruzione di riattivarlo. 5. La cancellazione fisica rimuove il record; una ricostruzione può ricrearlo se viene nuovamente inferito. 6. La ricostruzione è additiva: conserva le relationship attive già presenti, non rimuove quelle non più inferibili e non modifica le relationship manuali. 7. L'inferenza usa nomi, primary key e compatibilità dei tipi. Non campiona valori del DWH. 8. La relationship è l'unica fonte di verità: non viene duplicata in stringhe `fk_field` sulle colonne. 9. I nomi vengono confrontati senza distinzione fra maiuscole/minuscole e normalizzando snake case, kebab case e camel case. La regola riconosce anche casi come `user_id → users.id`, richiede tipi compatibili e una sola destinazione possibile; riconosce inoltre un nome PK non generico con un unico proprietario e la convenzione `*time_key → dim_time.`. Le colonne sorgenti possono appartenere a PK composite, mentre nomi generici isolati come `id`, `key`, `code` e `pk` non costituiscono evidenza. I casi ambigui vengono ignorati e non si usa fuzzy matching o un LLM. 10. La ricostruzione parte da un'azione amministrativa esplicita `Rebuild generated relationships`, separata dalla sincronizzazione dello schema. 11. Le relationship cancellate logicamente restano consultabili tramite filtro e possono essere riattivate con un'azione `Restore`. Non restano decisioni di dominio aperte per il flusso minimo. La progettazione UX e il seam tecnico sono descritti nelle sezioni seguenti. ## Lacuna UX emersa nel grill La vista Fleet `Relationships` esiste, ma nella UI di produzione non ha oggi un punto di ingresso raggiungibile. La riga del database espone sincronizzazione, tabelle, dettagli, modifica e rimozione, ma non le relationship; inoltre i tab `Overview / Tables / Relationships` appartengono soltanto alla presentazione legacy. Il test di navigazione esistente esercita anch'esso la presentazione legacy, non quella Fleet (`frontend/src/shell/database-management/DatabaseGrid.tsx:122-180`; `frontend/src/shell/database-management/DatabaseForm.tsx:442-475`; `frontend/src/shell/database-management/DatabaseForm.tsx:517-546`; `frontend/src/shell/DatabaseManagementPage.test.tsx:2635-2703`). Se aperta programmaticamente, la vista mostra soltanto `Physical relationships` in sola lettura. La toolbar contiene ricerca, conteggio, `Refresh`, un selettore azione con esecuzione esplicita e `Sync history`; la griglia offre unicamente `Details`, che apre il drawer della relationship fisica. Sono assenti ingresso visibile, aggiunta manuale, ricostruzione delle generated relationship, origine, stato attivo/cancellato, cancellazione logica, cancellazione fisica e ripristino (`frontend/src/shell/database-management/DatabaseRelationships.tsx:114-225`). I pattern Fleet già consolidati da riutilizzare sono: - una sola griglia nel livello corrente, con breadcrumb e controllo Back; - azioni di pagina nel selettore `Choose an action…` con `Run action` e motivo visibile quando indisponibili; - azioni della singola riga nella colonna finale fissata a destra; - form e dettagli in un drawer modeless che restituisce il focus al controllo di origine; - conferme distruttive inline o nel drawer, non tramite una nuova pagina; - toast per accettazione o errore e feedback persistente soltanto per le operazioni lunghe; - card di errore con Retry ed empty state che indica la prossima azione possibile. ThothAI non offre un modello UX da copiare. L'inferenza è nascosta fra 21 bulk action della lista database, non mostra avanzamento in tempo reale e restituisce soltanto messaggi a fine richiesta. Il CRUD manuale vive in un'altra schermata Django Admin, non distingue origine o stato, offre soltanto la cancellazione fisica e il form di aggiunta ha una validazione server strutturalmente incoerente con le select popolate dal browser (`Thoth/ThothAI/backend/thoth_core/admin_models/admin_sqldb.py:252-274`; `Thoth/ThothAI/backend/thoth_core/admin_models/admin_relationship.py:58-119`; `Thoth/ThothAI/backend/thoth_core/admin_models/admin_relationship.py:218-313`). ## UX confermata Il 31 agosto 2026 sono state confermate le seguenti scelte: 1. La colonna Actions della riga database espone un accesso diretto `Relationships`, accanto a `Tables`. La vista conserva breadcrumb e controllo `Back to databases` esistenti. 2. La pagina presenta una sola griglia `Relationship map`, contenente relationship `Physical`, `Generated` e `Manual`. Le colonne sono Source table, Source column, Target table, Target column, Origin, Status e Actions. Constraint e regole update/delete rimangono nel drawer delle FK fisiche. 3. Un filtro visibile seleziona `Active`, `Excluded` o `All`; il valore predefinito è `Active`. Le FK fisiche sono consultabili ma non modificabili da questa vista. 4. `Add relationship` è il pulsante primario visibile nella toolbar. Apre il drawer standard con i quattro campi Source table, Source column, Target table e Target column e i comandi `Cancel` e `Add relationship`. Il flusso minimo gestisce una sola coppia di colonne e mostra la validazione accanto al campo interessato. 5. `Rebuild generated relationships` entra nell'attuale selettore `Choose an action…`, insieme a `Synchronize physical relationships` e `Synchronize full schema`, con esecuzione esplicita tramite `Run action`. `Refresh` ricarica la griglia; `Sync history` resta riservato alla sincronizzazione fisica. 6. Il drawer di dettaglio contiene le azioni sulle relationship logiche. `Exclude` realizza la cancellazione logica, `Delete permanently` quella fisica e `Restore` riattiva una relationship esclusa. La conferma avviene nel drawer e spiega rispettivamente che la ricostruzione non riattiverà un record escluso e potrà invece ricreare un record eliminato definitivamente. 7. La ricostruzione non introduce un nuovo sistema di job o di storico. Durante l'esecuzione mostra `Rebuilding…`; al termine un toast riporta added, already present, excluded e ambiguous. Add, Exclude, Delete permanently e Restore producono toast specifici; gli errori mantengono aperto il contesto corrente. L'empty state propone di ricostruire dai nomi o aggiungere manualmente. 8. L'inferenza non usa AI. È codice deterministico nel backend del catalogo, basato su normalizzazione dei nomi, primary key/unicità, compatibilità dei tipi e assenza di ambiguità. Non usa LLM, embedding o campionamento dei dati. L'AI consuma la mappa risultante per comprendere lo schema, ma non la costruisce. ## Seam tecnico confermato Il Catalog PostgreSQL è l'unica fonte modificabile delle Logical Relationship. Le relationship fisiche e logiche restano in modelli distinti, coerentemente con ADR-0006, mentre un servizio profondo espone a API e UI una sola mappa discriminata per origine e stato. All'avvio o alla ripresa di una sessione, il backend materializza le sole relationship attive in una snapshot JSON canonica e immutabile, collegata alla stessa lease della configurazione runtime. La snapshot comprende anche le FK fisiche e conserva l'ordine delle coppie composite. Se due record hanno gli stessi endpoint, la FK fisica ha precedenza. Quando la snapshot è presente, l'harness la usa come fonte esclusiva delle relationship e continua a leggere dalle annotazioni Git-pinned soltanto descrizioni, sinonimi, concetti e altri metadati. Non scrive `annotations.yaml` e non interroga direttamente il Catalog. Una snapshot dichiarata ma assente, invalida o incoerente con lo schema fisico fallisce esplicitamente; un runtime legacy che non dichiara la snapshot mantiene il precedente comportamento di compatibilità. Questa proiezione non è un secondo store: non può essere modificata, viene eliminata insieme alla configurazione runtime e una sessione Pi vede una mappa stabile per tutta la propria vita. La decisione duratura è registrata in ADR-0012. La proiezione e la ricostruzione sono ammesse soltanto dopo una sincronizzazione completa della versione corrente del database e vengono serializzate con le mutazioni del catalogo. Un catalogo mai sincronizzato, reso stale da una modifica della configurazione o invalidato da metadata cleanup non può quindi diventare accidentalmente la fonte esclusiva del runtime. Il cleanup esplicito di una tabella o colonna endpoint è anche il confine distruttivo del tombstone: rimuove definitivamente la relationship esclusa, perché conservarla richiederebbe una seconda identità testuale denormalizzata.