feat: consolidate database management work

Add catalog-owned logical relationships and runtime snapshots, extend the database-management UI and validation coverage, and document the updated operational workflow.

Keep active sensitive-generation status in a tooltip and indicator, and update the layout E2E to follow the history action in its new database-scoped location.
This commit is contained in:
Codex
2026-09-01 14:46:55 +02:00
parent f586152636
commit 076c9742c5
73 changed files with 6966 additions and 610 deletions
@@ -0,0 +1,299 @@
# Visualizzatore ERD nel browser: proposta solo open source
Data dell'audit: 31 agosto 2026.
## Decisione
Per ThothII sceglierei **AntV X6 + ELK.js** come fondazione del visualizzatore ERD.
- **AntV X6** è MIT, non ha una distinta edizione Professional e include nel progetto OSS
le funzioni che servono davvero a un diagramma complesso: nodi e porte personalizzabili,
pan/zoom, selezione, minimappa, history, clipboard, tastiera, routing, virtual rendering ed
export SVG/PNG/JPEG.
- **ELK.js** è il motore di layout automatico, EPL-2.0 e senza tier commerciale. Il layout
layered gestisce porte, vincoli sull'ordine delle porte, archi multipli, self-loop e routing
ortogonale: proprietà importanti per foreign key composite e schemi affollati.
- La combinazione non dipende da esempi o componenti a pagamento. Il lavoro applicativo che
resta da fare — semantica ER, livelli di dettaglio, filtri e adattatore X6/ELK — appartiene
davvero al dominio di ThothII e non è una funzione nascosta dietro un piano Pro.
La seconda scelta è **React Flow + ELK.js**. È probabilmente l'integrazione più naturale con
l'attuale frontend React e ha una migliore storia documentata per accessibilità. Il runtime è
MIT e non viene tecnicamente depotenziato, ma diversi pattern avanzati già implementati
(auto-layout, edge routing, undo/redo, copy/paste, expand/collapse) sono pubblicati come esempi
con licenza React Flow Pro. Non è quindi la scelta più lineare se il criterio prioritario è
evitare anche una dipendenza progettuale da materiale Professional.
## Cosa significa «open source senza limitazioni Professional»
L'audit distingue tre casi:
1. **OSS completo**: licenza OSI e nessun tier commerciale che trattenga funzioni essenziali.
2. **OSS con caveat**: il motore è aperto, ma esistono esempi premium, API instabili o limiti
architetturali rilevanti. Può essere usato, purché il limite sia accettato esplicitamente.
3. **Escluso**: licenza proprietaria oppure core aperto con funzioni necessarie al viewer
complesso riservate alla versione commerciale.
La valutazione riguarda sia la licenza sia ciò che si può effettivamente costruire senza
acquistare un prodotto complementare. Le note sulle licenze sono tecniche, non consulenza legale.
## Matrice di selezione
| Soluzione | Licenza OSS | Tier Pro rilevante | Adeguatezza a ERD complessi | Verdetto |
|---|---|---|---|---|
| **AntV X6 + ELK.js** | MIT + EPL-2.0 | Nessuno individuato | Porte per colonna, minimappa, routing, export, virtual rendering e layout avanzato | **Consigliata** |
| **React Flow + ELK.js** | MIT + EPL-2.0 | Esempi/pattern avanzati Pro, non runtime separato | Ottima UX React, nodi HTML, minimappa e accessibilità; export SVG non nativo | **Valida con caveat** |
| **maxGraph** | Apache-2.0 | Nessuno | Molto completo, SVG, porte, folding e layout; API imperativa e integrazione React costosa | **Valida con caveat** |
| **Cytoscape.js + ELK** | MIT | Nessuno | Molto scalabile come grafo, ma meno adatto a tabelle ricche e porte per campo | **Alternativa di nicchia** |
| **Liam ERD/CLI** | Apache-2.0 | Nessuno | Viewer ERD già rifinito e self-hostable; il package embeddable è interno e instabile | **Reference o app separata** |
| **Graphviz + Viz.js** | EPL-2.0 + MIT | Nessuno | Layout ed SVG eccellenti; non offre da solo un explorer applicativo | **Renderer di export** |
| **Mermaid.js + Panzoom** | MIT + MIT/ISC | Mermaid Chart è separato | Facile, ma insufficiente per interazioni e modelli ER ricchi | **Solo preview semplice** |
| **Sprotty/GLSP** | EPL-2.0 | Nessuno | Potente e completo, ma è un framework di modeling più pesante del necessario | **Non prioritario** |
| **JointJS** | Core MPL-2.0 | Molte funzioni utili sono in JointJS+ | Lo split commerciale intercetta proprio le necessità del viewer | **Esclusa** |
| **GoJS** | Proprietaria | Licenza di deployment | Completa tecnicamente, ma non open source | **Esclusa** |
| **yFiles** | Proprietaria | Licenza commerciale | Completa tecnicamente, ma non open source | **Esclusa** |
## 1. Scelta principale: AntV X6 + ELK.js
### Funzioni disponibili nell'open source
X6 offre un canvas diagrammatico basato su SVG con supporto anche a nodi HTML/React. Il core
espone porte, archi personalizzabili, self-loop e multiedge. I plugin distribuiti dal progetto
comprendono Scroller, MiniMap, Selection, History, Clipboard, Keyboard, DnD, Stencil, Snapline,
Transform ed Export. La documentazione copre inoltre router `orth`, `manhattan` ed `er`, zoom e
panning. Non è emersa un'edizione X6 Pro che renda a pagamento queste capacità.
Fonti primarie: [repository e licenza X6](https://github.com/antvis/X6),
[porte](https://x6.antv.antgroup.com/en/tutorial/basic/port),
[router](https://x6.antv.antgroup.com/en/api/registry/router),
[minimappa](https://x6.antv.antgroup.com/en/tutorial/plugins/minimap),
[scroller](https://x6.antv.antgroup.com/en/tutorial/plugins/scroller) ed
[export](https://x6.antv.antgroup.com/en/tutorial/plugins/export).
ELK non è un renderer: calcola la geometria del grafo. L'algoritmo layered supporta porte e
relativi vincoli, archi ortogonali, self-loop e archi multipli. Va eseguito in un Web Worker per
non bloccare l'interfaccia sui modelli grandi. L'adattatore dovrà passare a ELK le porte delle
colonne e usare anche le `edge sections` e i bend point restituiti, non soltanto le coordinate
dei nodi.
Fonti primarie: [ELK.js e licenza](https://github.com/kieler/elkjs) e
[ELK layered](https://eclipse.dev/elk/reference/algorithms/org-eclipse-elk-layered.html).
### Limiti reali
- X6 è più imperativo di React Flow: conviene incapsularlo in un singolo componente React con
un adapter stabile, evitando di spargere istanze e listener nel resto dell'applicazione.
- L'accessibilità per screen reader non è documentata al livello di React Flow; tab order,
focus, descrizioni ARIA e navigazione da tastiera richiedono test e lavoro applicativo.
- Un nodo React/HTML può introdurre `foreignObject` nell'SVG. Per un export portabile e
stampabile è meglio produrre un SVG separato dallo stesso modello e dalla geometria ELK.
Questi sono costi tecnici, non limitazioni commerciali.
## 2. Seconda scelta: React Flow + ELK.js
`@xyflow/react` è MIT. Custom nodes React, handle multipli, pan/zoom, fit view, Controls,
MiniMap, selezione e funzioni di accessibilità sono nel runtime OSS. React Flow Pro vende
supporto, template ed esempi con relativo codice sorgente; non esiste una libreria runtime Pro
che sblocchi il canvas.
Il caveat è comunque concreto: auto-layout, edge routing, undo/redo, copy/paste ed
expand/collapse compaiono nel catalogo degli esempi Pro e quel codice usa la **xyflow Pro
License**, non la MIT del core. Le stesse funzioni possono essere sviluppate sopra le API OSS,
ma non si può considerare tutto il materiale ufficiale liberamente riutilizzabile.
Fonti primarie: [package React Flow](https://github.com/xyflow/xyflow/blob/main/packages/react/package.json),
[funzioni del core](https://reactflow.dev/index),
[catalogo degli esempi Pro](https://reactflow.dev/examples/pro-examples),
[auto-layout](https://reactflow.dev/examples/layout/auto-layout) e
[termini Pro](https://reactflow.dev/pro).
È una buona scelta se si privilegiano velocità d'integrazione React, componenti HTML e
accessibilità. Non è la prima scelta di questo audit perché l'utente ha chiesto espressamente
di minimizzare la distanza fra ciò che è open source e l'offerta Professional.
Altro limite: React Flow combina nodi DOM e archi SVG. L'esempio ufficiale di download usa
`html-to-image`, quindi il diagramma interattivo non si traduce automaticamente in un SVG puro.
## 3. Altre soluzioni interamente open source
### maxGraph
maxGraph, successore TypeScript di mxGraph, è Apache-2.0 e non ha un piano Pro. Offre rendering
SVG, connection constraints/porte, loop e multigraph, layout, routing, outline, grouping e
folding. Può quindi sostenere un editor ERD ricco.
Lo terrei come terza scelta: l'API è imperativa, non esiste un wrapper React ufficiale, la
virtualizzazione non è documentata e gran parte dell'accessibilità va costruita. Il progetto è
ancora pre-1.0. Il costo di integrazione e manutenzione sarebbe maggiore di X6.
Fonti: [licenza e repository](https://github.com/maxGraph/maxGraph),
[guida Vite/TypeScript](https://maxgraph.github.io/maxGraph/docs/getting-started/) e
[gestione della complessità](https://maxgraph.github.io/maxGraph/docs/usage/group-and-complexity-management/).
### Cytoscape.js
Cytoscape.js e molte estensioni sono MIT, senza tier professionale. È ottimo per grandi grafi,
filtri, selezioni e algoritmi di rete. Il rendering Canvas è però meno naturale per schede-tabella
ricche, testo selezionabile e handle collegati alle singole colonne. Inoltre l'adapter
`cytoscape.js-elk` non passa le porte a ELK né usa le route degli archi: non risolve da solo il
problema delle foreign key per colonna.
È appropriato per una vista di dipendenze ad alto livello, non come renderer ERD principale.
L'estensione `cytoscape-svg` è GPL-3.0; in un prodotto che non vuole assorbire quel vincolo è
preferibile una pipeline Graphviz/Viz.js o un generatore SVG proprio.
Fonti: [Cytoscape.js](https://github.com/cytoscape/cytoscape.js),
[adapter ELK](https://github.com/cytoscape/cytoscape.js-elk) e
[cytoscape-svg](https://github.com/kinimesi/cytoscape-svg).
### Liam ERD
Liam ERD è Apache-2.0, self-hostable e già orientato al problema: pan/zoom, ricerca, filtro,
command palette e modalità `TABLE_NAME`, `KEY_ONLY`, `ALL_FIELDS`. La documentazione dichiara
supporto a schemi con oltre cento tabelle. La CLI può generare un'app Vite statica.
Non userei però direttamente `@liam-hq/erd-core` dentro ThothII: il maintainer lo definisce una
dipendenza interna, ne sconsiglia l'uso diretto e avverte che l'API può cambiare; il package è
ancora 0.x. Inoltre occorre verificare la compatibilità con la versione React di ThothII e la
fedeltà delle foreign key composite. Liam è quindi un ottimo benchmark UX, una CLI per una vista
separata o una base da forkare consapevolmente, non l'interfaccia stabile su cui fondare il
prodotto.
Fonti: [repository Liam](https://github.com/liam-hq/liam),
[README di erd-core](https://github.com/liam-hq/liam/blob/main/frontend/packages/erd-core/README.md),
[funzioni UI](https://liambx.com/docs/ui-features) e [CLI](https://liambx.com/docs/cli).
### Sprotty/GLSP
Sprotty e Eclipse GLSP sono EPL-2.0 e offrono un'infrastruttura seria per diagrammi SVG,
layout e protocolli client/server. Sono pensati per strumenti di modeling estensibili, con
dependency injection e un'architettura più ampia di un viewer. Restano una soluzione OSS valida,
ma sproporzionata per questa esigenza salvo che ThothII evolva in un vero editor di modelli.
Fonti: [Sprotty](https://github.com/eclipse-sprotty/sprotty) ed
[Eclipse GLSP](https://eclipse.dev/glsp/documentation/overview/).
## 4. Renderer complementari, non fondazioni del viewer
### Graphviz e Viz.js
Graphviz è EPL-2.0; `@viz-js/viz`, il port WebAssembly utilizzabile nel browser, è MIT. Graphviz
produce SVG di alta qualità, supporta label HTML-like e porte e rimane molto utile per export,
stampa o una vista read-only. Non fornisce però da solo ricerca, filtri, livelli di dettaglio,
editing o gestione dello stato applicativo.
Lo userei come renderer di export alternativo, non come UI primaria. Se il layout a schermo
deve corrispondere esattamente all'export, è preferibile generare l'SVG direttamente dalla
geometria ELK invece di mantenere due motori di layout indipendenti.
Fonti: [licenza Graphviz](https://graphviz.org/license/),
[formati SVG](https://graphviz.org/docs/outputs/svg/) e
[Viz.js](https://github.com/mdaines/viz-js).
### Mermaid.js e librerie di pan/zoom
Mermaid.js è MIT e non ha feature del renderer open source bloccate. **Mermaid Chart** è invece
un servizio commerciale distinto e il suo piano gratuito ha limiti: non va confuso con la
libreria self-hosted.
Si può aggiungere navigazione a un SVG Mermaid con `@panzoom/panzoom` (MIT) o `d3-zoom` (ISC),
entrambi senza tier Pro. Questo migliora l'esperienza corrente, ma non supera i limiti strutturali
del diagramma ER Mermaid: interazioni a livello di tabella, controllo ridotto delle porte e del
routing, assenza di semantic zoom e difficoltà crescente su schemi molto grandi.
Mermaid resta adatto a preview e documentazione, non al viewer finale.
Fonti: [Mermaid.js](https://github.com/mermaid-js/mermaid),
[distinzione da Mermaid Chart](https://mermaid.ai/open-source/ecosystem/mermaid-chart.html),
[Panzoom](https://github.com/timmywil/panzoom) e [d3-zoom](https://github.com/d3/d3-zoom).
### Python
SchemaSpy ed ERAlchemy sono open source e possono generare documentazione o grafi attraverso
Graphviz, ma non sostituiscono il componente interattivo React. Python è utile lato backend per
normalizzare metadati o produrre artefatti batch; pan/zoom, selezione, ricerca e dettagli restano
responsabilità del browser. Aggiungere un servizio Python solo per disegnare l'ERD non porta un
vantaggio architetturale a ThothII.
## 5. Soluzioni escluse
### JointJS / JointJS+
Il core JointJS è MPL-2.0, ma JointJS+ è commerciale e comprende proprio molte funzioni che
servirebbero qui: PaperScroller, Navigator/minimappa, selection, clipboard, keyboard, undo/redo,
toolbar, export e layout aggiuntivi. Sarebbe tecnicamente possibile ricostruirle sul core, ma la
distanza fra OSS e Professional è troppo grande rispetto al criterio richiesto.
Fonti: [licenza](https://www.jointjs.com/license),
[confronto delle funzioni](https://www.jointjs.com/features) e
[prezzi](https://www.jointjs.com/pricing).
### GoJS e yFiles
Sono prodotti completi e maturi, ma non open source. GoJS richiede una licenza per il deployment
e mostra una filigrana senza chiave; yFiles usa licenze proprietarie, con evaluation temporanea e
licenza necessaria per la distribuzione. Non soddisfano il requisito, quindi non entrano nella
shortlist.
Fonti: [licensing GoJS](https://gojs.net/latest/intro/deployment.html) e
[licensing yFiles](https://docs.yworks.com/yfiles-html/dguide/deployment/licensing.html).
## 6. Architettura proposta per ThothII
Il frontend usa React 18 e oggi `SchemaLinkingViewer.tsx` genera Mermaid con un limite di 45
elementi. La resa corrente perde informazione: accorpa più foreign key fra la stessa coppia di
tabelle, omette i self-reference e usa cardinalità generiche. Il catalogo espone già tabelle,
colonne, chiavi primarie e relazioni con coppie ordinate di colonne, quindi può alimentare un
modello più fedele.
Propongo questa separazione:
1. **Modello renderer-neutral**: `TableNode`, `ColumnPort` e `RelationEdge` con nome del
constraint e lista ordinata delle coppie sorgente/destinazione. Non appiattire le FK composite.
2. **Snapshot API**: un endpoint coerente, per esempio
`GET /catalog/databases/:databaseId/schema-diagram`, che restituisca tabelle, colonne,
relazioni e versione del catalogo in una sola lettura, evitando la richiesta colonne N+1.
3. **Layout worker**: ELK.js in Web Worker, con porte per colonna, port constraints, route degli
archi e cache per versione del database e filtri correnti.
4. **Renderer interattivo**: X6 incapsulato in `DatabaseRelationshipDiagram.tsx`, affiancato
alla vista tabellare con un toggle List/Diagram e con riuso del drawer dei dettagli.
5. **Export**: generatore SVG separato basato sullo stesso modello e sulla geometria ELK;
Graphviz/Viz.js può essere un fallback per layout alternativi o documentazione batch.
Il catalogo non registra oggi tutti i vincoli UNIQUE né il tipo MATCH delle FK. Senza questi
dati non sempre è possibile distinguere correttamente 1:1 da 1:N. Il renderer non deve inventare
la cardinalità: deve mostrare una notazione neutra finché il metadato necessario non viene
raccolto.
### Funzioni necessarie per schemi complessi
- semantic zoom: solo nomi tabella, poi PK/FK, infine tutti i campi;
- ricerca e filtri per schema, tabella, colonna e tipo di relazione;
- modalità focus con vicinato a uno o due hop;
- evidenziazione della relazione e delle due colonne al passaggio/selezione;
- minimappa, fit selection, navigazione da tastiera e pannello dettagli;
- distinzione visiva di PK, FK, nullable, composite key, self-loop e relazioni parallele;
- layout automatico ricalcolabile, ma posizioni manuali persistibili;
- rendering progressivo o virtuale e fallback tabellare sempre disponibile;
- export dell'intero schema e dell'area filtrata in SVG/PNG.
## 7. Proof of concept consigliato
Un POC breve dovrebbe usare **X6 + ELK.js** su tre dataset sintetici: circa 30, 150 e 500
tabelle, includendo FK composite, più FK tra la stessa coppia, self-loop, tabelle isolate e hub
ad alto grado.
I criteri di accettazione dovrebbero misurare:
- tempo di layout nel worker e tempo al primo frame interattivo;
- fluidità di pan/zoom e uso memoria sul caso da 500 tabelle;
- correttezza di porte, archi paralleli, self-loop e FK composite;
- leggibilità nelle tre soglie di semantic zoom;
- navigazione completa da tastiera e comportamento con screen reader;
- qualità e portabilità dell'SVG esportato;
- assenza di codice, esempi o componenti soggetti a licenza commerciale.
Se X6 non raggiunge il livello di accessibilità richiesto senza un costo eccessivo, il confronto
finale va fatto con React Flow + lo stesso adapter ELK e lo stesso modello dati. In questo modo
si cambia renderer senza rifare API, semantica delle relazioni o pipeline di export.
@@ -0,0 +1,246 @@
# 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.<PK singola>`;
- 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 |
Le analisi già presenti nel repository trattano l'integrazione catalogo→core, la selezione pubblicabile e la riparazione degli indici come questioni ancora aperte; le loro proposte non sono decisioni implementate (`docs/research/2026-08-23-thothii-metadata-publication-qdrant-seams.md:20-43`; `docs/research/2026-08-23-thothii-metadata-publication-qdrant-seams.md:239-301`). Anche il piano di migrazione rinvia il lifecycle/admin delle relationship logiche (`docs/plans/2026-08-26-metadata-catalog-from-thothai.md:681-691`).
## 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.<PK singola>`. 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.