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:
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user