24 KiB
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
- 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). - Il ricordo di una relationship inferita persistita come
generatednon trova riscontro nel modello esaminato:Relationshipcontiene solo quattro foreign key verso tabelle e colonne, senza provenienza, stato, confidenza o flaggenerated. 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). - 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). - 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). - 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
Relationshipe 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.<PK singola>; - stesso nome tra colonna e primary key a proprietario univoco;
- assunzioni esplicite per disambiguare;
- esclusione di nomi PK generici come
id,keyecodee 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:
ForeignKeyammette liste di colonne, ma non valida che source e target abbiano la stessa cardinalità; il renderer usazip, 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
seendopo 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 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:
- Le foreign key dichiarate continuano a essere sincronizzate come Catalog Relationship fisiche.
- 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.
- Una relationship inferita è marcata
generated; una relationship aggiunta dall'utente non lo è. - La cancellazione logica conserva il record e impedisce a una ricostruzione di riattivarlo.
- La cancellazione fisica rimuove il record; una ricostruzione può ricrearlo se viene nuovamente inferito.
- La ricostruzione è additiva: conserva le relationship attive già presenti, non rimuove quelle non più inferibili e non modifica le relationship manuali.
- L'inferenza usa nomi, primary key e compatibilità dei tipi. Non campiona valori del DWH.
- La relationship è l'unica fonte di verità: non viene duplicata in stringhe
fk_fieldsulle colonne. - 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.<PK singola>. Le colonne sorgenti possono appartenere a PK composite, mentre nomi generici isolati comeid,key,codeepknon costituiscono evidenza. I casi ambigui vengono ignorati e non si usa fuzzy matching o un LLM. - La ricostruzione parte da un'azione amministrativa esplicita
Rebuild generated relationships, separata dalla sincronizzazione dello schema. - 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…conRun actione 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:
- La colonna Actions della riga database espone un accesso diretto
Relationships, accanto aTables. La vista conserva breadcrumb e controlloBack to databasesesistenti. - La pagina presenta una sola griglia
Relationship map, contenente relationshipPhysical,GeneratedeManual. 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. - Un filtro visibile seleziona
Active,ExcludedoAll; il valore predefinito èActive. Le FK fisiche sono consultabili ma non modificabili da questa vista. 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 comandiCanceleAdd relationship. Il flusso minimo gestisce una sola coppia di colonne e mostra la validazione accanto al campo interessato.Rebuild generated relationshipsentra nell'attuale selettoreChoose an action…, insieme aSynchronize physical relationshipseSynchronize full schema, con esecuzione esplicita tramiteRun action.Refreshricarica la griglia;Sync historyresta riservato alla sincronizzazione fisica.- Il drawer di dettaglio contiene le azioni sulle relationship logiche.
Excluderealizza la cancellazione logica,Delete permanentlyquella fisica eRestoreriattiva 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. - 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. - 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.