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,36 @@
# Use the Catalog as the logical relationship authority
ThothII stores database-declared foreign keys and user-managed Logical Relationships in separate
Catalog models, as required by ADR-0006, but exposes them through one effective relationship map.
Physical relationships remain read-only and are refreshed from the database. Logical relationships
are either Generated by deterministic column-name inference or Manual; inference does not call an
AI model and does not inspect source values.
A generated rebuild is additive. It preserves active and Manual relationships, never reactivates a
logically deleted relationship, and may recreate a relationship only after permanent deletion. A
logical deletion is therefore represented by retaining the relationship with an exclusion marker;
a permanent deletion removes it. Inference accepts only unambiguous, type-compatible matches to a
single-column primary key and skips all other candidates. It recognizes normalized table-qualified
names such as `user_id -> users.id`, exact non-generic primary-key names with one owner, and the
warehouse convention `*time_key -> dim_time.<single PK>`. A source column may participate in a
composite primary key; bare generic names such as `id`, `key`, `code`, and `pk` are not evidence by
themselves.
An exclusion is durable while both Catalog Column endpoints exist. Explicit metadata cleanup of an
endpoint table or column is a destructive boundary: it permanently removes every relationship and
exclusion attached to that endpoint, invalidates the synchronized-catalog marker, and requires a
full schema synchronization. The newly imported endpoints may then be inferred again. Preserving an
exclusion across endpoint destruction would require a second denormalized name-based identity, which
this design deliberately avoids.
The installation-local Catalog is the sole writable authority for Logical Relationships. Git-pinned
workspace annotations remain authoritative for descriptive metadata but their embedded foreign keys
are legacy compatibility data. The backend materializes the active effective map as an immutable,
deterministic runtime snapshot when a session starts or resumes. When that snapshot is present, the
harness uses it as the exclusive relationship source and ignores relationships embedded in both the
physical schema artifact and workspace annotations; a missing or invalid declared snapshot fails
closed. Legacy runtimes without a snapshot retain the previous merge behavior.
The snapshot is a projection, not another authored store. Its lifetime is tied to the runtime-config
lease, physical relationships take precedence over duplicate Logical Relationships, composite-key
column order is retained, and one Pi process observes one stable map for its complete lifetime.
+16 -3
View File
@@ -6,7 +6,7 @@ session workflow.
## What the catalog owns
For each YAML workspace, an administrator may create at most one database configuration. It holds
For each YAML workspace, an administrator may configure at most one Metadata Catalog binding. It holds
the database name, schema, connection binding, write-only encrypted secrets, observed physical
schema, optional curated descriptions, generated descriptions, and durable operation history.
@@ -27,6 +27,18 @@ It uses `GET /catalog/metrics` without `databaseId` for installation totals and
the current database. Choose a selection-scoped operation from the action selector and then press
**Run**; unavailable operations remain listed with an explanation. Row-specific actions are the icon
controls in the final column, and each navigation or action icon has an immediate conceptual tooltip.
An unconfigured workspace exposes **Configure catalog** directly on its row; there is no global
database-creation action and the selected workspace cannot be changed in the configuration form.
The master grid keeps three independent states visible:
- **Revision / Evidence** comes from the active immutable workspace revision. Filesystem Evidence is
materialized with that revision; remote Evidence is reported as configured-but-unverified or as
requiring credentials.
- **NL→SQL runtime** is calculated from the workspace DWH/Evidence requirements and runtime secret
store. It also reports transports, such as SSH, that are diagnostic-only and unsupported by sessions.
- **Metadata Catalog** reports whether the installation-local catalog configuration exists, then shows
its separately versioned connection-test or synchronization state.
Configuration, object details, metadata editors, synchronization history, description history,
sensitive-field review, and suggestion-run history open in right-side drawers backed by the
@@ -42,8 +54,9 @@ remains available only until the integrated Fleet Ledger surface passes owner ac
## Configure and test a database
1. Open **Database Management** and choose a workspace.
2. Create its PostgreSQL configuration. Choose `postgres_direct`, `rest_api`, or `ssh_tunnel` and
1. Open **Database Management** and find the repository workspace marked **Not configured**.
2. Choose **Configure catalog** on that row. Configure its PostgreSQL catalog binding with
`postgres_direct`, `rest_api`, or `ssh_tunnel` and
complete the binding fields that the chosen transport requires.
3. Enter secrets only when replacing them. They remain write-only and are never returned by the
application.
+43
View File
@@ -0,0 +1,43 @@
# ThothII Docker refresh
The active local `psd-local` stack uses the operator environment generated for the installation:
`deploy/psd/operator.env`
It is not `deploy/thothii.env` (that file is empty) and `deploy/env/local.env` is only an example
path referenced by the generic launcher. The running stack also uses these Compose overlays:
```text
compose.yaml
deploy/compose.local.yaml
deploy/compose.git-ssh.yaml
deploy/psd/connector-secrets.yaml
```
Refresh the stack from the repository root with:
```bash
compose_psd=(
docker compose
--env-file deploy/psd/operator.env
-p thothii-18998cca7b0a
-f compose.yaml
-f deploy/compose.local.yaml
-f deploy/compose.git-ssh.yaml
-f deploy/psd/connector-secrets.yaml
)
"${compose_psd[@]}" config --quiet
"${compose_psd[@]}" build core frontend
"${compose_psd[@]}" stop core frontend
"${compose_psd[@]}" up -d catalog-db
"${compose_psd[@]}" run --rm catalog-migrate
"${compose_psd[@]}" up -d --remove-orphans
```
Building before the stop keeps the existing application available if an image fails to compile.
Stopping only `core` and `frontend` prevents the old backend from using a newly migrated catalog;
the database, Qdrant, embedding service, named volumes, and installation state remain in place.
The installation secret files referenced by that env file live under `deploy/psd/secrets/` and
must never be committed or printed.
@@ -109,12 +109,13 @@ canonico; i percorsi e i comportamenti descrivono il sorgente disponibile il 202
conserva il valore esistente e la sostituzione è un'azione esplicita. Delete rimuove anche i
segreti associati.
34. La pagina usa AG Grid come master e un form React come detail, con sezioni Database, Connection
e TLS/SSH condizionali. La toolbar offre `Add database`; le righe `unconfigured` offrono
`Configure`. Entrambe selezionano esclusivamente workspace YAML senza un database e creano il
record soltanto al Save; `workspace_id` diventa immutabile dopo la creazione.
35. La grid mostra workspace, database, schema, transport, endpoint, stato connessione e ultimo
aggiornamento. Su schermi piccoli il dettaglio occupa il pannello completo. Il cambio riga con
modifiche non salvate e Delete richiedono conferma, senza conferma testuale tipizzata.
e TLS/SSH condizionali. Non esiste un'azione globale `Add database`: ogni riga `unconfigured`
offre `Configure catalog`, apre il form già vincolato a quello specifico workspace YAML e crea il
record soltanto al Save; `workspace_id` non è selezionabile né modificabile.
35. La grid mostra separatamente revisione/Evidence del workspace, binding runtime NL→SQL e
configurazione del Metadata Catalog, oltre a database, schema, endpoint e ultimo aggiornamento.
Su schermi piccoli il dettaglio occupa il pannello completo. Il cambio riga con modifiche non
salvate e Delete richiedono conferma, senza conferma testuale tipizzata.
36. La Database Binding conserva `connection_status`, `tested_version`, `last_tested_at`, un codice
errore e un messaggio breve sanificato. Non conserva stack trace, DSN, credenziali o output grezzo
del driver.
@@ -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.
@@ -0,0 +1,428 @@
# Piano di test: metadati di schema, campi sensibili e descrizioni AI
- Data: 2026-08-31
- Stato: proposto
- Baseline analizzata: `f586152` sul branch `test/database-baseline`
## 1. Obiettivo
Validare insieme le tre capacità recentemente introdotte in Database Management:
1. acquisizione autorevole dei metadati fisici di uno schema esterno;
2. proposta e revisione umana dei campi sensibili dal punto di vista privacy;
3. generazione, revisione e consolidamento delle descrizioni di tabelle e colonne.
Il piano è risk-based: perdita o corruzione di metadati, lettura o invio di valori protetti e
applicazione di una selezione al target sbagliato sono rischi bloccanti. La qualità linguistica dei
testi AI è invece valutata separatamente dal contratto tecnico, perché l'output del modello è una
proposta soggetta a revisione umana.
Questo documento non costituisce una certificazione normativa o GDPR: verifica i controlli tecnici
e il flusso operativo implementati dal prodotto.
### 1.1 Assunzione operativa: ambiente completamente sacrificabile
Per indicazione esplicita del proprietario, l'installazione sul Mac è esclusivamente di sviluppo e
test. Non contiene produzione e non esiste alcun requisito di conservazione dello stato locale.
- `catalog-db`, migrazioni, configurazioni locali, run, eventi, flag, descrizioni curate e generate
possono essere cancellati e ricreati tutte le volte necessarie;
- il database reale già collegato è la fonte autorevole dalla quale ricostruire il catalogo ed è la
sorgente primaria per validare schema, classificazione privacy e generazione delle descrizioni;
- reset completi, failure injection, dati incoerenti deliberati e prove distruttive sul catalogo
locale sono ammessi senza backup o procedura di rollback dell'ambiente;
- compatibilità con stato locale preesistente, sessioni legacy e vecchie revisioni del catalogo non
è un gate di questo piano;
- le prove di atomicità, idempotenza e preservazione dei campi restano necessarie perché verificano
il comportamento del prodotto dentro un ciclo di test, non perché debbano proteggere il Mac.
Il database sorgente resta normalmente read-only: quando una prova richiede DDL o mutazioni fra
scan e conferma si usa uno schema di test esplicitamente scrivibile o la fixture supplementare. La
disponibilità a buttare lo stato locale non elimina il rischio di inviare dati personali a un
provider esterno: proprio questa non-esfiltrazione è uno degli esiti principali da collaudare.
## 2. Terminologia e risultato atteso
I tre campi testuali del catalogo hanno autorità diverse e non devono essere confusi:
| Campo | Origine e autorità | Comportamento atteso |
| --- | --- | --- |
| `sourceComment` | Commento fisico acquisito dalla sorgente | Si aggiorna con la sincronizzazione e non è modificabile come contenuto curato. |
| `generatedDescription` | Proposta prodotta dall'AI o corretta nel catalogo | È salvata separatamente, può essere rigenerata esplicitamente e non sovrascrive gli altri due campi. |
| `description` | Descrizione curata dall'amministratore | Cambia solo tramite modifica esplicita o consolidamento di una proposta selezionata. |
Nel seguito, “commento generato” indica `generatedDescription`. La generazione non deve mai
modificare `sourceComment`; il passaggio a `description` avviene solo con il consolidamento umano.
## 3. Riferimenti e baseline
Il comportamento da verificare deriva da:
- stato corrente del progetto in `PROJECT_STATE.md`;
- [contratto Catalog Schema Snapshot](../contracts/catalog-schema-snapshot.md);
- [piano del Metadata Catalog](../plans/2026-08-26-metadata-catalog-from-thothai.md);
- [specifica della generazione descrizioni](../plans/2026-08-28-ai-catalog-description-generation-spec.md);
- [ADR 0007: sincronizzazione autorevole durevole](../adr/0007-durable-authoritative-schema-synchronization.md);
- [ADR 0009: un solo run sequenziale di generazione](../adr/0009-use-one-sequential-description-generation-run.md);
- [ADR 0010: campioni reali limitati](../adr/0010-allow-bounded-real-source-samples-for-description-generation.md);
- [ADR 0011: Sensitive Data Flag](../adr/0011-gate-source-samples-with-a-sensitive-data-flag.md);
- [accettazione AI del 2026-08-29](./2026-08-29-ai-catalog-description-generation-acceptance.md).
L'accettazione del 2026-08-29 è una baseline utile ma non chiude il gate attuale: precede i commit
`0736983` e `cb40c09`, inviava campioni reali inventati e documentava che un valore campione era
stato ripreso nella descrizione. Deve quindi essere ripetuta sul comportamento privacy corrente.
## 4. Perimetro
### Incluso
- trasporti `postgres_direct`, `rest_api` e `ssh_tunnel`;
- RPC REST tipizzato `POST /rpc/schema_snapshot` e fallback singolo read-only su
`POST /rpc/run_query`;
- metadati di tabelle, colonne, commenti sorgente, tipi, default, nullabilità, posizioni PK e coppie
FK ordinate;
- scope di sincronizzazione `tables`, `columns`, `relationships` e `all`;
- run durevoli, conferma delle differenze distruttive, cancellazione, recovery, eventi SSE e
fallback polling;
- Sensitive Data Flag, suggerimenti AI strutturali, review draft e storico operativo;
- scope di generazione `selected_columns`, `selected_tables`, `all` e `missing`;
- campionamento read-only, valori sintetici per colonne protette, batching, retry, stop, Unlock,
storico e consolidamento;
- uso controllato del database reale già collegato, inclusi schema e valori non sensibili, per il
collaudo end-to-end con fake provider e provider configurati;
- permessi, minimizzazione dei dati, redazione di log/errori e resistenza a input ostili.
### Escluso o rinviato
- applicazione del Sensitive Data Flag allo schema-linking/LSH del core, esplicitamente rinviata;
- sostituzione di `annotations.yaml`, pubblicazione Qdrant e cutover del runtime NL→SQL;
- alias, sinonimi, concetti, value descriptions e relazioni logiche;
- audit delle decisioni umane sul flag: per disegno si conserva solo il booleano corrente;
- DDL o scritture sul database sorgente;
- conservazione di cataloghi, run, descrizioni o sessioni locali precedenti al reset;
- compatibilità all'indietro con formati o migrazioni legacy non appartenenti alla baseline corrente.
## 5. Priorità e strategia
| Priorità | Significato | Esempi |
| --- | --- | --- |
| P0 | Gate bloccante di sicurezza o integrità | Nessuna lettura/invio di valori protetti; applicazione atomica; scope esatto; segreti non esposti. |
| P1 | Contratto funzionale necessario al rilascio | Trasporti, stati dei run, recovery, batching, review e consolidamento. |
| P2 | Qualità, UX e robustezza non distruttiva | Copy, focus, benchmark del modello, carico e degrado SSE. |
Le prove sono distribuite su quattro livelli:
1. **Contratto/unità**: repository in memoria, finti connector e Model Completer; nessuna rete.
2. **Integrazione**: catalogo PostgreSQL ricreabile via Docker, database reale come sorgente,
fixture supplementare, fake REST/SSH e Model Completer osservabile.
3. **UI/E2E locale**: component test con MSW e almeno un flusso Playwright sullo stack locale.
4. **Accettazione L2**: provider configurato realmente e database reale dopo review dei flag.
I test deterministici verificano il contratto. Le prove con un modello reale verificano
compatibilità e qualità, ma non sostituiscono i gate P0.
## 6. Ambiente e dati di test
### 6.1 Ambiente minimo
- stack locale con `catalog-db` eliminabile, migrazioni ripetibili e backend/frontend della stessa
baseline;
- database reale già collegato come sorgente primaria, con utenza capace di `SELECT` ma non di
`INSERT`, `UPDATE`, `DELETE`, DDL o cambio di schema;
- PostgreSQL effimero supplementare solo per le mutazioni controllate che non devono essere fatte
sul database reale;
- server REST fake in grado di servire snapshot valido, capability `unavailable`, 404, risposta
parziale/malformata e fallback `run_query`;
- server OpenSSH effimero con `known_hosts` esatto e varianti host key errata/assente;
- Model Completer fake che registra transitoriamente i messaggi e restituisce esiti programmabili;
- browser senza segreti in Web Storage e raccolta di log backend/SSE/API per le scansioni canary.
Le prove PostgreSQL di integrazione non devono risultare `skip`: la disponibilità di Docker è una
precondizione del gate. Il catalogo locale può essere azzerato prima di ogni wave senza snapshot o
backup del suo stato precedente.
### 6.2 Database reale e fixture supplementare `catalog_qa`
La prima baseline viene acquisita dal database reale collegato. Il test registra soltanto inventario
strutturale, conteggi e risultati sanitizzati; poi svuota il catalogo locale e dimostra di poterlo
ricostruire dalla stessa sorgente. Non è richiesto preservare alcun metadato locale precedente.
La fixture `catalog_qa` integra il database reale soltanto quando servono casi controllabili o
mutazioni che la sorgente reale non contiene. Deve includere almeno:
- `customers`: UUID PK, nome, email, codice fiscale, telefono, data di nascita, indirizzo e note;
- `orders`: FK verso `customers`, importo numerico, stato, timestamp, default e campi nullable;
- `order_lines`: PK composta e relazione composta ordinata;
- `clinical_events`: campi sanitari evidenti e tabella partizionata;
- `products`: SKU pubblico, categoria, prezzo e flag booleano;
- `empty_table`: nessuna riga ma struttura valida;
- `wide_entity`: almeno 23 colonne e metadati lunghi, per forzare batch `10 + 10 + 3` e il limite
dimensionale del messaggio;
- identificatori quotati, commenti Unicode/italiani, commenti null e oggetti fuori dallo schema.
Usare valori canary inventati e univoci, per esempio:
- `PRIV_EMAIL_CANARY_...`, `PRIV_TAX_CANARY_...`, `PRIV_HEALTH_CANARY_...` nelle colonne protette;
- `PUBLIC_SKU_CANARY_...` in una colonna esplicitamente non sensibile;
- `SECRET_API_CANARY_...` solo nel secret store del test.
I canary protetti non devono comparire nei messaggi al modello, nel catalogo, negli eventi, nelle
API, nel DOM o nei log. Il canary pubblico può apparire nel messaggio al provider entro i limiti
documentati e dopo la disclosure esplicita dell'utente. Sul database reale la stessa proprietà va
provata soprattutto osservando la proiezione SQL e il payload transitorio: i valori protetti non
devono essere letti, e nessun valore grezzo deve entrare nell'evidenza conservata.
### 6.3 Mutazioni della sorgente
Preparare tre revisioni dello schema:
- **A — iniziale**: struttura completa e commenti sorgente valorizzati;
- **B — distruttiva**: rimozione di una tabella, una colonna e una FK, più aggiunta di una nuova
colonna sensibile per nome;
- **C — race di conferma**: modifica ulteriore fra piano distruttivo e conferma, per provare il
re-scan.
Queste revisioni possono vivere nella fixture o in uno schema reale esplicitamente dichiarato
scrivibile. Fra una prova e l'altra è consentito eliminare completamente il catalogo locale,
riapplicare le migrazioni e ripartire dal database reale.
## 7. Casi di test — acquisizione dei metadati
| ID | P | Livello | Scenario | Risultato atteso |
| --- | --- | --- | --- | --- |
| MET-01 | P0 | API/Integrazione | Avvio senza binding raggiungibile, con versione database obsoleta o senza `database.manage`. | Il run non parte; risposta sicura e catalogo invariato. I segreti restano write-only. |
| MET-02 | P0 | Integrazione | Reset completo del catalogo e `all` sul database reale collegato, senza mock del client `pg_catalog`; ripetizione sulla fixture A per gli edge case assenti. | Il catalogo viene ricostruito da zero con snapshot esatta di tabelle, colonne, `sourceComment`, tipo, default, nullabilità, PK e FK ordinate; stato `succeeded`; nessuna scrittura alla sorgente. |
| MET-03 | P1 | Contratto/Integrazione | Stessa fixture via RPC REST tipizzato. | `schemaVersion: 1` e capability sono validate strettamente; risultato normalizzato uguale a MET-02. `unavailable` non è interpretato come collezione vuota. |
| MET-04 | P0 | Contratto/Integrazione | `/schema_snapshot` assente, fallback `run_query`; poi fallback assente, parziale, non JSON o con campi extra/mancanti. | Il fallback usa una sola query read-only. Ogni errore o snapshot invalida fallisce senza modifiche parziali; nessun fallback nasconde un errore operativo diverso da capability assente. |
| MET-05 | P1 | Integrazione | Accesso `ssh_tunnel` con host key corretta, errata e assente; errore durante apertura/chiusura. | Parità con MET-02 nel caso valido; fail-closed negli altri casi; processi, lease e file-segreto sempre rilasciati. |
| MET-06 | P0 | API | Esecuzione separata di `tables`, `columns`, `relationships` e `all`, con e senza selezione tabelle. | Ogni scope è autorevole solo nel proprio confine; nessun record fuori scope cambia o viene eliminato. Selezioni duplicate/inesistenti sono rifiutate atomicamente. |
| MET-07 | P0 | API/Integrazione | Ripetizione idempotente della fixture A dopo modifica di `description`, `generatedDescription` e `sensitive`. | Nessun diff fisico spurio; contenuti curati, proposte AI e flag delle entità ancora presenti sono preservati. Una nuova colonna nasce con `sensitive=false`. |
| MET-08 | P0 | API/Integrazione | Passaggio A→B, conferma assente/errata/scaduta e passaggio A→B→C prima della conferma. | Stato `awaiting_confirmation`, piano visibile e nessuna applicazione anticipata. La conferma valida provoca re-scan; se il diff cambia viene emesso un nuovo piano/token e il vecchio non applica nulla. Apply atomica oppure zero modifiche. |
| MET-09 | P0 | API/Integrazione | Timeout, disconnessione, capability incompleta o eccezione durante scan/apply. | Stato terminale coerente, errore sanitizzato, catalogo precedente intatto e lock rilasciato. Nessun segreto, SQL sensibile o stack trace nelle API/eventi. |
| MET-10 | P1 | Worker | Cancel in `queued`, `running`, `awaiting_confirmation` e `applying`; retry, restart con run attivo e lease scaduto. | Cancel è efficace solo prima di apply ed è rifiutato durante apply; retry crea un nuovo run. Recovery non duplica l'apply, marca correttamente i run interrotti, rilascia il lock e rimuove snapshot/diff/token interni non più necessari. |
| MET-11 | P0 | API | Due operazioni sullo stesso database: sync, cleanup, connection test, edit o generazione; in parallelo, operazioni su database diversi. | Una sola operazione possiede il database; conflitto 409 sicuro sullo stesso target. Nessun lock cross-database non previsto e nessuna release del token altrui. |
| MET-12 | P1 | UI | Avvio dai menu database/tabella, visualizzazione piano, conferma, history drawer, chiusura drawer, perdita SSE e polling. | Scope e selezione inviati sono esatti; azioni non eleggibili restano visibili con motivo; chiudere il drawer non ferma il run; replay/polling deduplicano gli eventi e aggiornano griglie/KPI. |
| MET-13 | P1 | E2E | Cleanup manuale di tabelle/colonne/relazioni e successiva sincronizzazione. | Cleanup modifica solo il catalogo; la sorgente resta invariata; un sync autorevole ripristina gli oggetti ancora presenti in sorgente. |
| MET-14 | P0 | Integrazione | Cambio binding/versione fra scan e apply ed errore iniettato a metà transazione. | La freshness viene ricontrollata sotto lock; il run fallisce senza righe parziali e conserva integralmente il catalogo precedente. |
| MET-15 | P1 | API/Worker | Replay SSE con `Last-Event-ID`/`after`, polling concorrente e retention oltre 30 giorni. | Cursori monotoni e nessun duplicato; gli eventi scaduti vengono potati senza corrompere run e stato finale. |
## 8. Casi di test — identificazione e protezione dei campi sensibili
| ID | P | Livello | Scenario | Risultato atteso |
| --- | --- | --- | --- | --- |
| PRV-01 | P0 | Repository/API | Prima sincronizzazione, re-sync e aggiunta di una colonna. | Il default è `false`; il valore umano delle colonne esistenti è preservato; la nuova colonna è esplicitamente da riesaminare ma non riceve uno stato audit inventato. |
| PRV-02 | P0 | API | Suggerimento per un database, tabelle selezionate e colonne selezionate; database multipli, target duplicati o mancanti. | Il provider riceve esattamente le colonne dello scope. Input ambigui sono rifiutati prima della chiamata e nessun flag cambia. |
| PRV-03 | P0 | Contratto | Ispezione del messaggio al classifier. | Sono presenti solo database, schema, tabella, colonna, tipo, nullabilità, PK e FK. Non compaiono righe, valori, commenti, descrizioni, flag corrente o segreti. |
| PRV-04 | P1 | Contratto | `wide_entity`, identificatori lunghi e limite byte. | Ordine deterministico, batch massimi di dieci colonne e rispetto del limite messaggio; una singola colonna non rappresentabile fallisce prima del provider con errore sicuro. |
| PRV-05 | P0 | API | Risposta valida, fenced/prosa, JSON malformato, target mancante/duplicato/ignoto e provider failure. | Ogni colonna richiesta compare una sola volta. Una classificazione invalida viene ritentata una volta; dopo esaurimento si ottiene errore sanitizzato e nessuna modifica. |
| PRV-06 | P0 | UI/API | Apertura draft, modifica manuale, chiusura/reload e salvataggio. | La proposta non è persistita prima di Save; reload la scarta. Il reviewer può invertire scelte; si salvano solo colonne cambiate con versione ottimistica; un conflitto richiede reload. |
| PRV-07 | P1 | Repository/UI | Tentativi completati, falliti e attivi al restart. | Ogni tentativo ha un run distinto con scope, modello, contatori ed eventi sanitizzati; startup marca `interrupted` i run attivi. Storico newest-first senza target ID, proposte, prompt, output grezzo o diagnostica provider. |
| PRV-08 | P0 | Integrazione | Generazione descrizioni su target con canary protetti. | Le colonne protette sono assenti dalla proiezione SQL, non semplicemente filtrate dopo la lettura. Se non rimangono colonne leggibili non viene eseguita una `SELECT`. Nessun canary protetto esce dal processo. |
| PRV-09 | P0 | Contratto/Integrazione | Tabella mista con colonne sensibili e pubbliche. | Per le sensibili il prompt contiene valori plausibili, deterministici e limitati derivati dai soli metadati, nello stesso formato dei campioni e senza etichettarli al modello come sintetici. Per le pubbliche: massimo cinque righe e cinque valori rappresentativi, valori troncati e transazione read-only chiusa con rollback. |
| PRV-10 | P1 | API | Cambio `false→true→false` dopo una descrizione già generata. | Il testo esistente non viene rigenerato retroattivamente. Solo le generazioni future cambiano fonte del contesto; tornando `false` il campionamento reale torna eleggibile. |
| PRV-11 | P0 | API/UI | Utente senza `database.manage`, modello non configurato, catalogo/provider indisponibile e richiesta interrotta. | Controlli nascosti/disabilitati in UI e rifiuto server-side; errori non espongono dettagli. Un tentativo fallito compare nello storico senza trasformarsi in audit della decisione umana. |
| PRV-12 | P1 | L2 | Corpus strutturale etichettato con identificatori personali, credenziali/token, salute, finanza, localizzazione e controlli non sensibili/ambigui, in inglese e italiano. | Si misurano precisione, recall e falsi negativi per modello. I campi critici mancati sono sottoposti al product owner; la soglia quantitativa va ratificata prima di diventare gate, perché il classifier è advisory e human-in-the-loop. |
| PRV-13 | P0 | UI/E2E | Modifica di un flag nella review senza Save e tentativo immediato di generare descrizioni. | Gate di rilascio da formalizzare: la generazione deve essere bloccata finché il draft non è salvato o scartato. In alternativa la UI deve dichiarare inequivocabilmente che verrà usato il valore persistito; non è accettabile mostrare “protetto” e campionare come non protetto. |
## 9. Casi di test — generazione e consolidamento dei commenti
| ID | P | Livello | Scenario | Risultato atteso |
| --- | --- | --- | --- | --- |
| GEN-01 | P0 | Config/API | Modelli validi, default, endpoint anonimo esplicito, secret ref mancante/errato e nessun modello. | Al browser arrivano solo ID, label e default. Nessuna chiave, provider payload o configurazione privata è esposta; feature disabilitata in modo comprensibile se non configurata. |
| GEN-02 | P0 | API | `selected_columns`, `selected_tables`, `all`, `missing`, target duplicati/inesistenti e zero eleggibili. | Scope esatto; `missing` include null/vuoto/whitespace e salta proposte esistenti; `all` sostituisce solo dopo conferma esplicita; input invalido non crea run. |
| GEN-03 | P1 | Worker | 23 colonne più tabelle, metadati e campioni lunghi. | Colonne prima delle tabelle per lo scope globale; richieste omogenee, sequenziali, massimo dieci target e sotto i limiti byte; contesto tabella aggiornato dopo le colonne. |
| GEN-04 | P0 | Contratto | JSON puro, un solo code fence JSON completo, prosa extra, payload multipli, target mancante/duplicato/ignoto, descrizione vuota o oltre limite. | Sono accettati solo i primi due formati validi. Una mappatura ambigua non applica alcun risultato del batch e conta come errore tecnico. |
| GEN-05 | P1 | API | Esito `generated` e `non_generatable` in workspace italiano/inglese. | Testo generato trimmato e salvato; il testo standard non generabile è localizzato dall'applicazione, non copiato dal provider. Gli errori tecnici non scrivono tale testo. |
| GEN-06 | P0 | Worker | Errore transiente, errore esaurito isolato, tre batch falliti consecutivi e successo fra due errori. | Helper con al massimo un retry e nessun fallback modello. Errori isolati portano a `completed_with_errors`; tre consecutivi a `failed`; un successo azzera il contatore. |
| GEN-07 | P0 | Integrazione | Successi seguiti da cancel, interruption o failure. | Ogni risultato valido è persistito subito e resta disponibile; il target fallito non riceve dati ambigui. “Generate Missing” consente il recupero naturale senza resume automatico. |
| GEN-08 | P0 | API | Secondo run durante un run attivo e modifica/sync/cleanup/consolidamento sul database posseduto. | Un solo run di generazione attivo nell'installazione; 409 chiaro al secondo Start. Il database target resta riservato e le operazioni incompatibili non alterano selezione o dati. |
| GEN-09 | P0 | Integrazione/Worker/UI | Stop in queued/running con helper attivo e con una `SELECT` sorgente deliberatamente bloccata/lenta. | Helper e query/connessione vengono terminati entro 5 secondi, stato `cancelled`, nessuna chiamata modello dopo Stop, risultati precedenti conservati ed eventi consultabili. Un test con sampler fake non è sufficiente. |
| GEN-10 | P0 | Worker/API | Restart con run `queued/running`; Unlock con worker/helper vivo e con run realmente stale; race Unlock/Start. | Startup marca `interrupted` senza replay. Unlock è rifiutato se esiste lavoro locale vivo e non può liberare la reservation di un nuovo run. |
| GEN-11 | P1 | Integrazione | Sorgente campioni indisponibile ma metadati validi. | La generazione prosegue metadata-only con un solo warning sicuro; nessun tentativo alternativo espone dettagli di connessione. |
| GEN-12 | P1 | API/UI | SSE disconnesso, replay da sequence, polling concorrente e riapertura storico. | Eventi persistiti, ordinati e deduplicati; history newest-first; contatori/stato finali coerenti; nessun prompt, campione, risposta completa o stack trace. |
| GEN-13 | P0 | UI/API | Revisione manuale della proposta e consolidamento selettivo di tabelle/colonne, inclusi target vuoti/stale. | Si copia solo `generatedDescription` non vuota dei target risolti; conteggio `copied/skipped` corretto; operazione atomica; `sourceComment` invariato. |
| GEN-14 | P0 | UI/Sicurezza | Tutti gli scope di generazione da database, tabelle e colonne selezionate, utente senza permesso, metadata contenente istruzioni ostili. | Prima di ogni Start, inclusi `selected_tables` e `selected_columns`, compare la disclosure “fino a cinque righe e cinque valori”. Il server applica comunque l'autorizzazione. Metadati e valori sono trattati come dati non fidati e l'output resta nel contratto JSON. |
| GEN-15 | P1 | L2 | Run reale sul modello di default e almeno un endpoint alternativo supportato, usando il database reale dopo la review dei flag e un role read-only. | Descrizioni nella lingua workspace, coerenti con struttura/commenti e senza fatti inventati critici; almeno una colonna e una tabella consolidate. Nessun valore protetto o segreto compare in request osservabile, eventi, API, persistenza o log. |
| GEN-16 | P1 | Integrazione | Fastify→worker→processo Python→LiteLLM→endpoint OpenAI-compatible locale di cattura. | Routing provider/model, header API key, `disableThinking`, singolo retry, timeout, limite output e payload sono corretti; chiave e diagnostica non risalgono a stdout, API o log applicativi. |
## 10. Percorso E2E prioritario
Il caso `E2E-01` deve attraversare le tre feature senza sostituire i test di contratto:
1. azzerare `catalog-db`, riapplicare le migrazioni e verificare che il role del database reale sia
realmente read-only;
2. eseguire `all` sul database reale e confrontare catalogo e snapshot sorgente; usare la fixture A
in una seconda esecuzione per gli edge case mancanti;
3. richiedere suggerimenti privacy sull'intero database;
4. modificare almeno una proposta e salvare i flag revisionati;
5. avviare `missing` dopo la disclosure, usando un Model Completer osservabile;
6. dimostrare che i canary protetti non sono letti né inviati e che il canary pubblico rispetta i
limiti;
7. correggere una `generatedDescription` e consolidare una tabella e una colonna;
8. applicare la fixture B, controllare il piano distruttivo e introdurre C prima della conferma;
9. confermare dopo il re-scan e verificare atomicità, preservazione di descrizioni/flag delle entità
superstiti e `sensitive=false` sulla nuova colonna;
10. perdere la connessione SSE, riaprire entrambi gli storici e verificare replay, polling e KPI.
Il percorso deve essere eseguito con fake provider dopo ogni reset rilevante e, in forma ridotta,
con il provider configurato sul database reale dopo che i flag sono stati revisionati e salvati.
Non è richiesto ripristinare lo stato locale precedente al test.
## 11. Valutazione qualitativa dei modelli
La qualità non deve confondersi con la sicurezza: anche un classifier perfetto non autorizza
l'esfiltrazione di un canary protetto e una descrizione elegante non rende valido un payload
ambiguo.
### 11.1 Classificazione privacy
Per ogni modello registrare matrice di confusione, precisione, recall e falsi negativi per categoria.
Eseguire almeno tre iterazioni sul corpus fisso per rilevare instabilità. Fino alla ratifica di una
soglia da parte del product owner, il risultato è un gate di review: ogni falso negativo su
credenziali/token, identificatori fiscali, dati sanitari o finanziari richiede accettazione esplicita
o correzione prima del rilascio operativo.
### 11.2 Descrizioni generate
Valutare ogni testo da 0 a 2 su:
- correttezza rispetto a struttura e commenti sorgente;
- specificità e utilità per un revisore;
- lingua e chiarezza;
- assenza di istruzioni seguite dai dati non fidati o fatti inventati;
- assenza di valori protetti e segreti.
Soglia proposta da ratificare: almeno 8/10, nessun punteggio 0 su correttezza o sicurezza. Un valore
esplicitamente non sensibile, reale o inventato, può essere ripreso entro il perimetro dichiarato;
un valore protetto non può mai esserlo.
## 12. Copertura esistente e gap da chiudere
| Area | Evidenza automatica già presente | Gap principale |
| --- | --- | --- |
| Snapshot e sincronizzazione | `backend/test/catalog-schema-introspector.test.ts`, `catalog-schema-routes.test.ts`, `catalog-table-introspector.test.ts`, `catalog-repository.integration.test.ts` | Introspezione `pg_catalog` realmente end-to-end, parità live dei tre trasporti e un unico E2E con re-scan distruttivo. |
| Privacy | `catalog-description-generation-routes.test.ts`, `catalog-description-generation-worker.test.ts`, `catalog-description-source-sampler.test.ts`, `catalog-synthetic-sample-value.test.ts` | Prova canary integrata query→prompt→API/log e benchmark reale post-ADR 0011. |
| Generazione | `catalog-description-generation-routes.test.ts`, `catalog-description-generation-worker.test.ts`, `catalog-description-generation.integration.test.ts` e test del helper | Accettazione reale aggiornata, cancellazione di una query PostgreSQL bloccata e integrazione ermetica fino all'endpoint LiteLLM locale. |
| UI | `DatabaseManagementPage.test.tsx`, `DescriptionGenerationDrawer.test.tsx`, `SensitiveDataSuggestionHistoryDrawer.test.tsx` | L'E2E Playwright corrente verifica soprattutto il layout, non il workflow funzionale. |
Nuovi asset consigliati:
- `backend/test/catalog-metadata-privacy-workflow.integration.test.ts`;
- `backend/test/fixtures/catalog-privacy-schema.sql` e snapshot REST equivalenti;
- integrazione a due PostgreSQL per le query reali `pg_catalog` e lo stop del sampler;
- endpoint OpenAI-compatible locale di cattura per GEN-16;
- `frontend/e2e/database-management-workflow.spec.ts`;
- corpus strutturale versionato per PRV-12, senza valori business;
- nuovo report di accettazione L2 che sostituisca il gate privacy del 2026-08-29.
## 13. Ordine di esecuzione
### Wave 1 — contratto rapido
- parser snapshot, introspector, scope e diff;
- classifier strutturale, batching e validazione output;
- sampler, valori sintetici, prompt bounds e parser descrizioni;
- autorizzazione, redazione e race del coordinator.
### Wave 2 — integrazione PostgreSQL
- reset totale di `catalog-db`, bootstrap delle migrazioni e ricostruzione dal database reale;
- migrazioni e vincoli del repository;
- atomicità sincronizzazione/consolidamento;
- run ed eventi persistiti, restart e cancellation;
- `E2E-01` con fake provider e canary.
### Wave 3 — frontend e stack locale
- component test MSW;
- Playwright funzionale, perdita SSE e polling;
- verifica disclosure, review draft, history e consolidamento.
### Wave 4 — accettazione L2
- provider reale sul database collegato dopo classificazione e review dei flag;
- benchmark PRV-12 e rubric GEN-15;
- scansione finale di canary e segreti;
- approvazione del product owner.
Comandi di regressione:
```bash
cd backend && npx vitest run
cd backend && npx tsc --noEmit -p .
cd backend && npm run build
cd frontend && npx vitest run
cd frontend && npx tsc -b
cd frontend && npm run build
cd frontend && npm run e2e
./scripts/build-docs.sh
```
Il report deve evidenziare esplicitamente eventuali test Docker/L2 saltati; un `skip` non equivale a
PASS del relativo gate.
## 14. Evidenze da conservare
Per ogni esecuzione registrare:
- commit, configurazione pubblica dei modelli, versione fixture e trasporto;
- ID e stato finale dei run, contatori ed eventi sanitizzati;
- snapshot catalogo prima/dopo e piano distruttivo con metadati e conteggi sanitizzati, senza valori
grezzi delle righe sorgente;
- report test/JUnit, screenshot dei gate UI e risultato della scansione canary;
- matrice di confusione privacy e rubric delle descrizioni per le prove L2;
- difetti con ID del caso, severità, riproducibilità e decisione finale.
Non allegare prompt completi, righe campione, output grezzi del provider, chiavi, digest o frammenti
di segreti. Il Model Completer spy deve verificare in memoria le asserzioni e scartare il payload al
termine del test.
Non serve conservare backup del catalogo locale, run precedenti o descrizioni generate durante una
wave: l'evidenza è il report sanitizzato e la capacità di ricostruire nuovamente il risultato dalla
sorgente reale.
## 15. Criteri di ingresso e uscita
### Ingresso
- baseline unica per backend/frontend e procedura verificata per eliminare e ricreare `catalog-db`;
- accesso al database reale collegato e inventario delle tabelle/colonne da includere nella review;
- fixture e canary supplementari approvati per i soli edge case controllati;
- role sorgente read-only verificato con una scrittura deliberatamente negata;
- fake connector/provider disponibili e log collection attiva;
- per L2, secret reference configurato senza materializzare il valore nell'evidenza.
### Uscita
- 100% dei casi P0 e P1 applicabili superati; nessun difetto Sev-1/Sev-2 aperto;
- almeno due ricostruzioni complete e coerenti del catalogo a partire dal database reale dopo reset
indipendenti;
- zero comparsa dei canary protetti e dei segreti fuori dalla sorgente/secret store del test;
- nessuna modifica parziale dopo errori, cancel o conferme stale;
- parità normalizzata dei trasporti supportati e nessuna integration PostgreSQL richiesta saltata;
- stati, contatori, eventi e history coerenti dopo success, partial failure, stop e restart;
- `sourceComment`, `generatedDescription` e `description` mantengono l'autorità prevista;
- accettazione L2 sul database collegato approvata dal product owner e
build/test/typecheck/documentazione verdi;
- ogni deviazione P2 o soglia qualitativa non ancora ratificata è documentata con owner e data.
## 16. Rischi residui da rendere espliciti
- `sensitive=false` è il default e il prodotto non conserva uno stato “review completata”: dopo ogni
reset, classificazione strutturale e salvataggio umano dei flag devono precedere qualunque run con
provider reale. Il catalogo è ricostruibile; un invio errato a un provider non lo è.
- Il flag protegge i valori campionati. Nomi, `sourceComment` e descrizioni sono metadati inviabili
al modello e possono contenere testo libero: se nel database reale includono PII serve una
decisione aggiuntiva di redazione, non una diversa aspettativa di test.
- La UI deve risolvere il caso di un flag modificato ma non salvato prima della generazione e deve
mostrare la disclosure anche per tabelle/colonne selezionate; il piano considera entrambi P0.
- L'AbortSignal corrente va provato contro una query PostgreSQL realmente bloccata: la sola
cancellazione del helper non dimostra che la lettura sorgente sia interrompibile.
- Lo storico dei Sensitive Data Suggestion Run è operativo, non un audit delle decisioni umane.
- La policy privacy non è ancora applicata allo schema-linking/LSH; nessun risultato di questo piano
deve essere presentato come copertura di quel percorso.
- Un provider reale resta non deterministico: il rilascio deve dipendere dai gate tecnici e dalla
review umana, non dalla ripetizione byte-identica delle descrizioni.
- L'esclusione fra operazioni e Unlock è in parte locale al processo. Se il deployment ammetterà più
repliche backend, servirà un gate aggiuntivo con due istanze contro lo stesso catalogo; non va
dedotta sicurezza multi-replica dai test single-process.
File diff suppressed because it is too large Load Diff