feat(frontend): add database management surface
This commit is contained in:
+8
-3
@@ -246,9 +246,10 @@ precedente o di un altro workspace.
|
||||
|
||||
## Catalogo dei metadati
|
||||
|
||||
**Workspace Database** — Il database associato a un workspace, considerato nella sua
|
||||
interezza fisica: tutte le tabelle, le colonne e le relazioni disponibili, anche quando
|
||||
solo un loro sottoinsieme è destinato al core di ThothII.
|
||||
**Workspace Database** — Il database associato in modo uno-a-uno a un workspace,
|
||||
considerato nella sua interezza fisica: tutte le tabelle, le colonne e le relazioni
|
||||
disponibili. La sua struttura fisica viene acquisita interrogando il database; il
|
||||
Metadata Catalog non crea né possiede l'identità del workspace.
|
||||
|
||||
**Metadata Catalog** — Il contesto amministrativo che raccoglie e cura i metadati di un
|
||||
Workspace Database. Non definisce quali elementi partecipano al workflow SQL.
|
||||
@@ -256,6 +257,10 @@ Workspace Database. Non definisce quali elementi partecipano al workflow SQL.
|
||||
**Database Profile** — L'insieme curato di scope, descrizioni e metadati semantici
|
||||
associato a un Workspace Database.
|
||||
|
||||
**Physical Schema Snapshot** — L'inventario della struttura fisica osservata in un
|
||||
Workspace Database durante una specifica introspezione. Non è un progetto dello schema
|
||||
né un'autorizzazione a modificarne la struttura.
|
||||
|
||||
**AI Proposal** — Un contenuto generato con l'ausilio dell'AI che non è ancora stato
|
||||
approvato come contenuto canonico.
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# PostgreSQL come autorità dei metadati dei database dei workspace
|
||||
|
||||
ThothII userà un Metadata Catalog PostgreSQL interno per conservare, per ogni workspace, la
|
||||
struttura fisica acquisita interrogando il relativo database e i metadati semantici generati con
|
||||
l'AI. Ogni workspace avrà un solo Workspace Database; identità e lista dei workspace resteranno
|
||||
autorevoli in `thoth-workspaces.yaml`, mentre il catalogo ne conserverà soltanto il riferimento
|
||||
stabile. `schema/annotations.yaml` verrà sostituito come input del core in uno step successivo;
|
||||
l'interfaccia e il lifecycle amministrativi resteranno separati dal workflow NL→SQL.
|
||||
|
||||
## Considered Options
|
||||
|
||||
- Conservare `annotations.yaml` come fonte di verità avrebbe mantenuto il revisionamento Git, ma
|
||||
non avrebbe fornito il CRUD e il processo di introspezione/generazione richiesti.
|
||||
- Leggere metadati mutabili dal catalogo senza un confine esplicito avrebbe accoppiato il workflow
|
||||
alla disponibilità del nuovo servizio e consentito viste parziali durante gli aggiornamenti.
|
||||
|
||||
## Consequences
|
||||
|
||||
PSD importerà le annotations esistenti; gli altri workspace genereranno i metadati da zero. Il
|
||||
cutover futuro dovrà sostituire consapevolmente i consumatori delle annotations e verificarne
|
||||
l'equivalenza semantica. Le sessioni di test esistenti non sono un vincolo di migrazione.
|
||||
@@ -0,0 +1,492 @@
|
||||
# Metadata Catalog di ThothII: ricognizione ThothAI e percorso incrementale
|
||||
|
||||
Data: 2026-08-26
|
||||
Stato: ricognizione completata; step 1 implementato; scelte tecnologiche degli step successivi
|
||||
deliberatamente rinviate.
|
||||
|
||||
## Obiettivo
|
||||
|
||||
ThothII deve introdurre un contesto amministrativo separato, il **Metadata Catalog**, per gestire
|
||||
il database associato a ciascun workspace, la sua struttura fisica introspezionata e i metadati
|
||||
semantici oggi rappresentati da `schema/annotations.yaml`.
|
||||
|
||||
Il programma procede per step indipendenti. Il primo step aggiunge soltanto l'accesso dalla sidebar
|
||||
destra a una superficie centrale vuota. Non introduce PostgreSQL, API CRUD, introspezione o
|
||||
integrazioni con il workflow core.
|
||||
|
||||
Questa analisi usa come riferimento il working tree legacy osservato in
|
||||
`Thoth/ThothAI`. Non è stato verificato che quel contenuto corrisponda a una release o a un tag
|
||||
canonico; i percorsi e i comportamenti descrivono il sorgente disponibile il 2026-08-26.
|
||||
|
||||
## Decisioni già confermate
|
||||
|
||||
1. Ogni workspace è associato a un solo Workspace Database e ogni Workspace Database appartiene a
|
||||
un solo workspace.
|
||||
2. Il CRUD non crea né rinomina workspace. Identità e lista ordinata dei workspace restano
|
||||
autorevoli in `thoth-workspaces.yaml`; il catalogo conserva il loro identificatore stabile.
|
||||
3. La struttura fisica viene acquisita interrogando il database esterno tramite i dati di
|
||||
connessione registrati per il Workspace Database.
|
||||
4. I contenuti semantici equivalenti a `annotations.yaml` vengono generati con l'AI e conservati nel
|
||||
PostgreSQL interno.
|
||||
5. Per PSD è prevista l'importazione delle annotations esistenti. Gli altri database partiranno
|
||||
dalla struttura introspezionata e genereranno i metadati semantici da zero.
|
||||
6. `annotations.yaml` sarà sostituito anche come input del core in uno step futuro. Il repository è
|
||||
in fase di test e non è richiesta la conservazione delle sessioni esistenti durante il cutover.
|
||||
7. La gestione catalogo resta una superficie separata dal processo NL→SQL. La futura integrazione
|
||||
deve essere esplicita e non deve modificare fasi, gate o semantica del workflow.
|
||||
8. Il link iniziale è visibile agli utenti con `workspace.manage`, usa stato React locale e non
|
||||
introduce un router.
|
||||
9. La pagina iniziale è vuota, segue il tema, nasconde l'intera colonna core e non interrompe una
|
||||
sessione live. Le azioni di apertura, resume o creazione sessione riportano al core.
|
||||
|
||||
## Correzione del modello mentale corrente
|
||||
|
||||
`schema/annotations.yaml` non contiene l'intero schema del database.
|
||||
|
||||
- `physical.yaml` è un artefatto derivato dall'introspezione. Contiene database, schema, timestamp,
|
||||
tabelle, colonne, tipi, nullability, default, primary key, commenti sorgente, esempi, foreign key
|
||||
fisiche e indici.
|
||||
- `annotations.yaml` contiene metadati curati: descrizioni e concetti delle tabelle; descrizioni,
|
||||
sinonimi, concetti, evidence, note e override `eligible` delle colonne; foreign key logiche.
|
||||
- Il rendering M-Schema fonde questi due input. Le annotations prevalgono sui commenti sorgente e
|
||||
le relazioni logiche vengono unite alle foreign key fisiche.
|
||||
|
||||
La sostituzione del solo file annotations non elimina automaticamente l'introspezione fisica. Il
|
||||
nuovo catalogo dovrà conservare una distinzione esplicita fra fatti osservati nel database e
|
||||
contenuto semantico modificabile.
|
||||
|
||||
## Architettura ThothII rilevante
|
||||
|
||||
### Autorità e revisionamento attuali
|
||||
|
||||
Il repository dei workspace contiene:
|
||||
|
||||
```text
|
||||
thoth-workspaces.yaml
|
||||
<workspace-id>/workspace.yaml
|
||||
<workspace-id>/schema/annotations.yaml
|
||||
<workspace-id>/evidence/**
|
||||
```
|
||||
|
||||
Il backend legge descriptor e annotations allo stesso commit Git. Durante l'attivazione valida il
|
||||
blob, lo copia atomicamente nello snapshot immutabile della revisione e registra commit, blob ID e
|
||||
digest. Le nuove sessioni vengono legate a quella revisione; resume e SQL salvato riaprono lo stesso
|
||||
snapshot.
|
||||
|
||||
Punti principali:
|
||||
|
||||
- `backend/src/workspaces/schema.ts`: descriptor v3 e singolo `dwh.database`/`dwh.schema`;
|
||||
- `backend/src/workspaces/git-repository.ts`: lettura sicura del blob annotations al commit;
|
||||
- `backend/src/workspaces/registry.ts`: validazione e attivazione atomica;
|
||||
- `backend/src/workspaces/annotations-sync.ts`: materializzazione revision-qualified;
|
||||
- `backend/src/workspaces/runtime-config-lease.ts`: binding dello snapshot al runtime;
|
||||
- `harness/tht/mschema/models.py`: contratti `PhysicalSchema` e `Annotations`;
|
||||
- `harness/tht/mschema/render.py`: fusione fisico/semantico;
|
||||
- `harness/tht/cli/vector_cmd.py`: indicizzazione schema in Qdrant.
|
||||
|
||||
### Consumatori da preservare al cutover futuro
|
||||
|
||||
Le annotations incidono oggi su:
|
||||
|
||||
- override `eligible` prima del campionamento LSH;
|
||||
- suggerimento, controllo e accettazione delle foreign key logiche;
|
||||
- descrizioni, concetti e sinonimi dei record schema in Qdrant;
|
||||
- retrieval delle tabelle e colonne candidate;
|
||||
- rendering M-Schema usato dal gate F4 e dalla generazione SQL;
|
||||
- digest della revisione accettata durante il preprocessing.
|
||||
|
||||
Il futuro cutover non potrà limitarsi a rimuovere il file: dovrà fornire al core lo stesso contenuto
|
||||
effettivo, con un'identità coerente e test di equivalenza. Poiché non occorre preservare le sessioni
|
||||
di test esistenti, non serve progettare compatibilità con i vecchi manifest, ma resta necessario
|
||||
evitare letture parziali o semanticamente incoerenti.
|
||||
|
||||
## Inventario ThothAI
|
||||
|
||||
### Modelli legacy
|
||||
|
||||
I modelli sono definiti in `Thoth/ThothAI/backend/thoth_core/models.py`.
|
||||
|
||||
#### `SqlDb`
|
||||
|
||||
Campi di connessione osservati:
|
||||
|
||||
- `name`;
|
||||
- `db_host`, `db_port`;
|
||||
- `db_type`;
|
||||
- `db_name`, `schema`;
|
||||
- `user_name`, `password`;
|
||||
- `db_mode`;
|
||||
- configurazione SSH e Informix opzionale.
|
||||
|
||||
Il modello contiene anche scope, JSON dello scope, ERD, direttive, campi GDPR, collegamento a
|
||||
`VectorDb` e numerosi campi di stato/task/log per lavori AI asincroni.
|
||||
|
||||
I tipi legacy dichiarati sono Informix, MariaDB, MySQL, Oracle, PostgreSQL, SQL Server e SQLite.
|
||||
Questo elenco non costituisce automaticamente un requisito per ThothII: il core corrente supporta
|
||||
PostgreSQL e l'estensione ad altri dialetti dovrà essere decisa separatamente.
|
||||
|
||||
#### `SqlTable`
|
||||
|
||||
- `name`;
|
||||
- `description`;
|
||||
- `generated_comment`;
|
||||
- foreign key obbligatoria a `SqlDb`, con cancellazione cascade.
|
||||
|
||||
#### `SqlColumn`
|
||||
|
||||
- `original_column_name` e alias `column_name`;
|
||||
- `data_format` normalizzato;
|
||||
- `column_description`;
|
||||
- `generated_comment`;
|
||||
- `value_description`;
|
||||
- stringhe denormalizzate `pk_field` e `fk_field`;
|
||||
- foreign key obbligatoria a `SqlTable`, con cancellazione cascade.
|
||||
|
||||
#### `Relationship`
|
||||
|
||||
Contiene quattro foreign key obbligatorie:
|
||||
|
||||
- `source_table` e `source_column`;
|
||||
- `target_table` e `target_column`.
|
||||
|
||||
Il form admin verifica che le tabelle appartengano allo stesso database e che ogni colonna
|
||||
appartenga alla tabella selezionata. Il database non impone però gli stessi check.
|
||||
|
||||
#### `Workspace`
|
||||
|
||||
ThothAI usa `Workspace.sql_db` come foreign key nullable verso `SqlDb`: un workspace seleziona un
|
||||
solo DB, mentre lo stesso DB può essere riusato da più workspace. ThothII adotterà invece una
|
||||
relazione uno-a-uno: `workspace_id` deve essere unico nel catalogo.
|
||||
|
||||
### Lacune dei constraint legacy
|
||||
|
||||
Non risultano constraint database-level per:
|
||||
|
||||
- unicità del nome database nel workspace;
|
||||
- unicità `(database, table name)`;
|
||||
- unicità `(table, column name)`;
|
||||
- unicità degli estremi di una relationship;
|
||||
- appartenenza degli estremi della relationship allo stesso database;
|
||||
- corrispondenza fra colonna e tabella dichiarata.
|
||||
|
||||
ThothII deve applicare queste invarianti sia nel database interno sia nel servizio applicativo. La
|
||||
sola validazione del form non è sufficiente perché API, import e job la possono aggirare.
|
||||
|
||||
### Django Admin e UX da replicare concettualmente
|
||||
|
||||
ThothAI espone il CRUD tramite il Django Admin standard, registrato da
|
||||
`backend/thoth_core/admin.py` e pubblicato su `/admin/`.
|
||||
|
||||
Capacità utili:
|
||||
|
||||
- lista database con ricerca per nome, host, tipo, database e schema;
|
||||
- fieldset separati per identità, connessione, autenticazione, SSH e stato;
|
||||
- lista tabelle filtrabile per database;
|
||||
- lista colonne filtrabile in cascata per database e tabella;
|
||||
- lista relazioni con estremi leggibili e filtri per database e tabelle;
|
||||
- form relazione con dropdown dipendenti database → tabella → colonna;
|
||||
- validazione degli estremi prima del salvataggio;
|
||||
- azioni separate per test connessione, introspezione, import/export e generazione AI;
|
||||
- azioni bulk sulle righe selezionate.
|
||||
|
||||
ThothII deve replicare i contratti di interazione e validazione, non il rendering server-side o i
|
||||
template Django.
|
||||
|
||||
### Introspezione legacy
|
||||
|
||||
`Thoth/ThothAI/backend/thoth_core/dbmanagement.py` usa `thoth-dbmanager` per:
|
||||
|
||||
1. costruire l'adapter del dialetto;
|
||||
2. acquisire tabelle;
|
||||
3. acquisire e normalizzare colonne e tipi;
|
||||
4. acquisire relazioni;
|
||||
5. creare le eventuali colonne mancanti necessarie alle relazioni;
|
||||
6. aggiornare i campi PK/FK denormalizzati.
|
||||
|
||||
Il comportamento è principalmente additivo: usa `get_or_create` o controlli `exists`, aggiorna
|
||||
alcuni commenti, ma non riconcilia in modo completo rename, rimozioni o drift. Non va copiato così
|
||||
com'è. Il futuro processo ThothII dovrà almeno distinguere scansione, differenze osservate e
|
||||
applicazione della nuova snapshot.
|
||||
|
||||
### Generazione AI legacy
|
||||
|
||||
ThothAI dispone di azioni e workflow per:
|
||||
|
||||
- commenti delle tabelle;
|
||||
- commenti delle colonne;
|
||||
- scope del database;
|
||||
- ERD Mermaid;
|
||||
- documentazione del database;
|
||||
- analisi GDPR.
|
||||
|
||||
Per il requisito attuale sono direttamente rilevanti descrizioni di tabelle e colonne, scope e
|
||||
metadati semantici. ERD, documentazione aggregata e GDPR sono estensioni future, non prerequisiti
|
||||
del CRUD iniziale.
|
||||
|
||||
La separazione `description`/`generated_comment` del legacy non offre versioning o approvazione
|
||||
robusti. Nei passi successivi andrà deciso se l'output AI è una proposta revisionabile o diventa
|
||||
immediatamente il valore editabile corrente.
|
||||
|
||||
### Import ed export legacy
|
||||
|
||||
ThothAI offre:
|
||||
|
||||
- CSV di database, tabelle, colonne e relazioni;
|
||||
- export di struttura per workspace;
|
||||
- import mediante `import_db_structure`;
|
||||
- script SQL dei commenti per più dialetti;
|
||||
- aggiornamento delle descrizioni colonna da CSV.
|
||||
|
||||
Il futuro import PSD dovrà leggere il contratto YAML corrente e convertirlo su chiavi naturali,
|
||||
non riutilizzare gli ID numerici Django. Deve essere idempotente e produrre un report di elementi
|
||||
creati, aggiornati, ignorati o non risolti.
|
||||
|
||||
## Comandi osservati in ThothAI
|
||||
|
||||
### Backend locale
|
||||
|
||||
Eseguiti da `Thoth/ThothAI/backend`:
|
||||
|
||||
```sh
|
||||
uv sync
|
||||
uv run python manage.py migrate
|
||||
uv run python manage.py createsuperuser
|
||||
uv run python manage.py runserver 8200
|
||||
uv run pytest
|
||||
```
|
||||
|
||||
Import catalogo legacy:
|
||||
|
||||
```sh
|
||||
uv run python manage.py import_db_structure --source local
|
||||
uv run python manage.py load_defaults --only-level 4 --source local
|
||||
```
|
||||
|
||||
Test mirati rilevanti:
|
||||
|
||||
```sh
|
||||
uv run pytest tests/test_relational_database_operations.py -v
|
||||
uv run pytest tests/test_ssh_tunnel_configuration.py -v
|
||||
```
|
||||
|
||||
### Stack Docker legacy
|
||||
|
||||
ThothAI dichiara `postgres:16-alpine` nel profilo `internal-db`, con volume persistente e
|
||||
healthcheck `pg_isready`.
|
||||
|
||||
```sh
|
||||
docker compose --profile internal-db up --build
|
||||
```
|
||||
|
||||
Il wrapper legacy abilita lo stesso profilo quando `POSTGRES_INTERNAL=true`:
|
||||
|
||||
```sh
|
||||
POSTGRES_INTERNAL=true ./docker-up.sh
|
||||
```
|
||||
|
||||
Questi comandi documentano il riferimento osservato; non sono comandi di installazione per
|
||||
ThothII.
|
||||
|
||||
## Cosa copiare in ThothII
|
||||
|
||||
### Parità necessaria
|
||||
|
||||
- gerarchia Workspace Database → Table → Column;
|
||||
- relazione strutturale fra colonne sorgente e destinazione;
|
||||
- navigazione e filtri dipendenti workspace/database/tabella;
|
||||
- test di connessione separato dal salvataggio;
|
||||
- introspezione esplicita e ripetibile;
|
||||
- descrizioni generate dall'AI ma modificabili dall'utente;
|
||||
- validazione cross-entity delle relazioni;
|
||||
- azioni di import/export senza segreti;
|
||||
- stato leggibile dei job lunghi;
|
||||
- PostgreSQL interno persistente con migrazioni esplicite;
|
||||
- test di CRUD, cardinalità, cascade/restrict, isolamento per workspace e idempotenza.
|
||||
|
||||
### Parità semantica con `annotations.yaml`
|
||||
|
||||
Il modello futuro deve poter rappresentare almeno:
|
||||
|
||||
- descrizione, concetti e note per tabella;
|
||||
- descrizione, sinonimi, concetti, evidence, note ed `eligible` per colonna;
|
||||
- foreign key logiche;
|
||||
- distinzione fra commento fisico osservato e descrizione curata;
|
||||
- provenienza del contenuto importato o generato.
|
||||
|
||||
L'eventuale esclusione di uno di questi campi deve essere una decisione esplicita perché cambia
|
||||
rendering, retrieval, LSH o SQL generation.
|
||||
|
||||
### Vincoli minimi da progettare
|
||||
|
||||
- `workspace_id` unico sul Workspace Database;
|
||||
- nome tabella unico nel database e schema appropriato;
|
||||
- nome colonna unico nella tabella;
|
||||
- relationship unica secondo il modello, anche per chiavi composite;
|
||||
- estremi della relationship nello stesso Workspace Database;
|
||||
- appartenenza certa della colonna alla tabella;
|
||||
- mutazioni aggregate transazionali;
|
||||
- gestione esplicita di concorrenza fra CRUD e introspezione.
|
||||
|
||||
## Cosa non copiare
|
||||
|
||||
- Django, Django Admin, Django ORM, DRF, template admin e frontend Next;
|
||||
- modello Workspace legacy e condivisione dello stesso DB fra più workspace;
|
||||
- password o passphrase come normali campi testuali;
|
||||
- password incluse in CSV o export completi;
|
||||
- token SSO inseriti nella query string;
|
||||
- migrazioni generate automaticamente all'avvio;
|
||||
- validazioni presenti soltanto nel form;
|
||||
- `pk_field` e `fk_field` testuali come fonte di verità;
|
||||
- duplicazione di tabella e colonna negli estremi senza constraint coerenti;
|
||||
- introspezione additiva che non segnala rename, delete o drift;
|
||||
- azioni admin che possono mostrare successo dopo output AI non valido;
|
||||
- dipendenza del workflow core dalla disponibilità della UI o del PostgreSQL amministrativo.
|
||||
|
||||
## Aspetti di sicurezza da non ereditare
|
||||
|
||||
L'export legacy della struttura include username e password in chiaro. Il modello conserva inoltre
|
||||
password, passphrase SSH e altri segreti in `CharField`; non è stata trovata cifratura applicativa,
|
||||
nonostante un testo admin affermi il contrario.
|
||||
|
||||
Per ThothII resta da decidere nello step infrastrutturale quali dati di connessione siano normali
|
||||
metadati e quali siano secret reference. In ogni caso:
|
||||
|
||||
- nessun endpoint o export deve restituire segreti;
|
||||
- log ed errori devono sanificare DSN e credenziali;
|
||||
- le credenziali di migrazione non devono essere disponibili al runtime CRUD;
|
||||
- il catalogo non deve riusare credenziali del DWH, delle sessioni o di Qdrant;
|
||||
- test connessione e introspezione devono usare timeout e privilegi read-only.
|
||||
|
||||
## Percorso incrementale
|
||||
|
||||
### Step 1: accesso alla superficie vuota
|
||||
|
||||
Implementato in questo worktree:
|
||||
|
||||
- pulsante `Database management` nella sidebar destra;
|
||||
- visibilità legata a `workspace.manage`;
|
||||
- superficie centrale React separata e vuota;
|
||||
- nessun router, endpoint, fetch o stato catalogo;
|
||||
- sessione e SSE conservati in background;
|
||||
- ritorno al core tramite creazione, apertura o resume di una sessione;
|
||||
- test frontend dedicati.
|
||||
|
||||
Comandi di verifica:
|
||||
|
||||
```sh
|
||||
cd frontend
|
||||
npx vitest run src/shell/AppShell.database-management.test.tsx
|
||||
npx vitest run src/shell/AppShell.new-session.test.tsx \
|
||||
src/shell/AppShell.session-target.test.tsx \
|
||||
src/shell/AppShell.session-mgmt.test.tsx
|
||||
npx tsc -b
|
||||
```
|
||||
|
||||
### Step 2: contratto di dominio e schema relazionale
|
||||
|
||||
Da progettare con un nuovo round decisionale: campi, secret reference, dialetti supportati,
|
||||
namespace/schema, snapshot fisiche, relazioni fisiche/logiche e lifecycle dell'output AI. Nessuna
|
||||
tecnologia ORM o migration tool è stata scelta in questo documento.
|
||||
|
||||
### Step 3: PostgreSQL interno e migrazioni
|
||||
|
||||
Da progettare separatamente dal core: servizio, volume, ruoli runtime/migrator/backup, health e
|
||||
readiness dedicati, backup/restore e diagnostica. La sua indisponibilità non dovrà cambiare
|
||||
`core /health` o interrompere una sessione.
|
||||
|
||||
### Step 4: API CRUD
|
||||
|
||||
Contratti HTTP, autorizzazione, paginazione, filtri, errori, optimistic concurrency e transazioni.
|
||||
Gli endpoint dovranno vivere sotto un namespace catalogo e non riutilizzare le route sessione.
|
||||
|
||||
### Step 5: UI CRUD
|
||||
|
||||
Liste e form per Workspace Database, tabelle, colonne e relazioni, costruiti con React/Vite e il
|
||||
design system ThothII. La gerarchia e i filtri ThothAI sono il riferimento funzionale; Django Admin
|
||||
non è il riferimento tecnologico o visuale.
|
||||
|
||||
### Step 6: introspezione
|
||||
|
||||
Connessione read-only, preview delle differenze, acquisizione di una Physical Schema Snapshot,
|
||||
policy per rename/rimozioni e stato del job. Nessuna chiamata lunga dovrà mantenere aperta una
|
||||
transazione CRUD.
|
||||
|
||||
### Step 7: generazione AI dei metadati
|
||||
|
||||
Generazione di descrizioni e altri campi equivalenti alle annotations, editing umano e gestione
|
||||
esplicita di errori o output non validi. Approvazione/versioning saranno decisi in questo step.
|
||||
|
||||
### Step 8: migrazione PSD
|
||||
|
||||
Import idempotente delle annotations PSD, riconciliazione contro la struttura introspezionata,
|
||||
report degli orfani e confronto semantico con il rendering corrente. Gli altri workspace non
|
||||
ricevono import legacy.
|
||||
|
||||
### Step 9: sostituzione dell'input core
|
||||
|
||||
Rimuovere la dipendenza da `annotations.yaml` soltanto dopo avere un contratto equivalente,
|
||||
test di rendering/search/Qdrant e una policy di disponibilità. Le sessioni di test esistenti
|
||||
possono essere eliminate, ma le nuove sessioni non devono osservare aggiornamenti parziali.
|
||||
|
||||
### Step 10: operazioni e accettazione
|
||||
|
||||
Backup/restore reale, diagnostica, metriche, audit, permessi definitivi, hardening degli export e
|
||||
test di failure isolation fra catalogo e workflow.
|
||||
|
||||
## Verifiche del core da conservare per il cutover
|
||||
|
||||
Comandi attuali rilevanti:
|
||||
|
||||
```sh
|
||||
tht --installation <absolute>/thothii-installation.yaml workspace preprocess dwh \
|
||||
--workspace <id> --json
|
||||
tht --installation <absolute>/thothii-installation.yaml workspace schema suggest-fks \
|
||||
--workspace <id> --json
|
||||
tht --installation <absolute>/thothii-installation.yaml workspace schema check \
|
||||
--workspace <id> --json
|
||||
tht --installation <absolute>/thothii-installation.yaml workspace schema accept \
|
||||
--workspace <id> --run <run-id> --yes --json
|
||||
tht --installation <absolute>/thothii-installation.yaml workspace index-schema \
|
||||
--workspace <id> --json
|
||||
```
|
||||
|
||||
Suite che documentano il comportamento da preservare:
|
||||
|
||||
```sh
|
||||
cd backend
|
||||
npx vitest run test/workspaces-git-annotations.test.ts \
|
||||
test/registry-annotations.test.ts \
|
||||
test/annotations-sync.test.ts \
|
||||
test/workspace-runtime-config-lease.test.ts \
|
||||
test/workspace-preprocessing-service.test.ts
|
||||
npx tsc --noEmit -p .
|
||||
|
||||
cd ../harness
|
||||
.venv/bin/pytest -q \
|
||||
tests/test_annotations_root.py \
|
||||
tests/test_schema_fk_annotations.py \
|
||||
tests/test_mschema_render.py \
|
||||
tests/test_qdrant_cli_commands.py
|
||||
```
|
||||
|
||||
Questi test non implicano che la futura implementazione debba continuare a usare file YAML.
|
||||
Definiscono gli effetti semantici e le guardie da mantenere o sostituire consapevolmente.
|
||||
|
||||
## Decisioni rinviate
|
||||
|
||||
Le seguenti scelte non appartengono allo step 1:
|
||||
|
||||
- framework del servizio catalogo e libreria di accesso PostgreSQL;
|
||||
- collocazione e protezione delle credenziali dei Workspace Database;
|
||||
- supporto iniziale di dialetti diversi da PostgreSQL;
|
||||
- uno o più schema namespace per database;
|
||||
- policy di reconciliation per rename e delete;
|
||||
- modello delle foreign key composite;
|
||||
- distinzione persistente fra relationship fisiche e logiche;
|
||||
- lifecycle draft/review/approval dell'output AI;
|
||||
- versionamento, audit e rollback;
|
||||
- formato e momento del cutover dal file al database interno;
|
||||
- permission definitiva separata da `workspace.manage`.
|
||||
|
||||
Ognuna sarà affrontata nel relativo step, senza anticipare scelte tecnologiche nel presente
|
||||
documento.
|
||||
@@ -0,0 +1,139 @@
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { http, HttpResponse } from "msw";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { server } from "../test/msw";
|
||||
import { FakeEventSource } from "../test/fakeEventSource";
|
||||
import { useSessionStore } from "../store/sessionStore";
|
||||
import { clearAuthState, setAuthState } from "../auth/authState";
|
||||
import { AppShell } from "./AppShell";
|
||||
|
||||
function renderShell() {
|
||||
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
return render(
|
||||
<QueryClientProvider client={client}>
|
||||
<AppShell canLogout={false} />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
clearAuthState();
|
||||
setAuthState({
|
||||
issuer: "test",
|
||||
subject: "test",
|
||||
roles: ["user"],
|
||||
permissions: ["session.use", "workspace.manage"],
|
||||
isAdmin: false,
|
||||
csrfToken: null,
|
||||
session: null,
|
||||
});
|
||||
localStorage.clear();
|
||||
FakeEventSource.instances = [];
|
||||
(globalThis as any).EventSource = FakeEventSource;
|
||||
useSessionStore.getState().resetSession();
|
||||
server.use(
|
||||
http.get("/api/me", () => HttpResponse.json({
|
||||
issuer: "test",
|
||||
subject: "test",
|
||||
displayName: "Test",
|
||||
isAdmin: false,
|
||||
})),
|
||||
http.get("/api/sessions", () => HttpResponse.json([])),
|
||||
http.get("/api/settings", () => HttpResponse.json({
|
||||
workspace: "default",
|
||||
provider: "test",
|
||||
model: "test",
|
||||
thinking: "low",
|
||||
})),
|
||||
http.get("/api/workspaces", () => HttpResponse.json([])),
|
||||
http.get("/api/models", () => HttpResponse.json({ models: [] })),
|
||||
http.post("/api/runtime/prewarm", () => new HttpResponse(null, { status: 202 })),
|
||||
);
|
||||
});
|
||||
|
||||
test("opens the blank database management surface and returns to the core", async () => {
|
||||
renderShell();
|
||||
|
||||
const composer = screen.getByRole("textbox", { name: /new question/i });
|
||||
await userEvent.type(composer, "Unsent draft");
|
||||
|
||||
const databaseManagement = screen.getByRole("button", { name: "Database management" });
|
||||
await userEvent.click(databaseManagement);
|
||||
|
||||
expect(screen.getByRole("main", { name: "Database management" })).toBeVisible();
|
||||
expect(databaseManagement).toHaveAttribute("aria-current", "page");
|
||||
expect(databaseManagement).toHaveClass("bg-sidebar-accent", "font-bold");
|
||||
expect(screen.queryByRole("textbox", { name: /new question/i })).not.toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByRole("button", { name: "New session" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole("main", { name: "Database management" })).not.toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByRole("textbox", { name: /new question/i })).toHaveValue("Unsent draft");
|
||||
});
|
||||
|
||||
test("keeps a live core session connected and returns when that session is opened", async () => {
|
||||
server.use(
|
||||
http.get("/api/sessions", () => HttpResponse.json([{
|
||||
id: "s1",
|
||||
status: "open",
|
||||
question: "Active question",
|
||||
summary: null,
|
||||
created_at: "2026-01-02T00:00:00Z",
|
||||
updated_at: null,
|
||||
author: "test",
|
||||
name: null,
|
||||
group: null,
|
||||
archived: false,
|
||||
active: true,
|
||||
}])),
|
||||
http.post("/api/sessions/:id/resume", ({ params }) => HttpResponse.json({
|
||||
id: params.id,
|
||||
alreadyActive: true,
|
||||
})),
|
||||
http.get("/api/sessions/:id", ({ params }) => HttpResponse.json({
|
||||
id: params.id,
|
||||
status: "open",
|
||||
phase: 1,
|
||||
})),
|
||||
);
|
||||
renderShell();
|
||||
|
||||
const session = await screen.findByTestId("session-item-s1");
|
||||
await userEvent.click(session);
|
||||
await waitFor(() => expect(FakeEventSource.instances).toHaveLength(1));
|
||||
const source = FakeEventSource.instances[0];
|
||||
|
||||
await userEvent.click(screen.getByRole("button", { name: "Database management" }));
|
||||
|
||||
expect(screen.getByRole("main", { name: "Database management" })).toBeVisible();
|
||||
expect(source.closed).toBe(false);
|
||||
expect(FakeEventSource.instances).toHaveLength(1);
|
||||
|
||||
await userEvent.click(session);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole("main", { name: "Database management" })).not.toBeInTheDocument();
|
||||
});
|
||||
expect(source.closed).toBe(false);
|
||||
expect(FakeEventSource.instances).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("hides database management from users without workspace management permission", () => {
|
||||
clearAuthState();
|
||||
setAuthState({
|
||||
issuer: "test",
|
||||
subject: "test",
|
||||
roles: ["user"],
|
||||
permissions: ["session.use"],
|
||||
isAdmin: false,
|
||||
csrfToken: null,
|
||||
session: null,
|
||||
});
|
||||
|
||||
renderShell();
|
||||
|
||||
expect(screen.queryByRole("button", { name: "Database management" })).not.toBeInTheDocument();
|
||||
});
|
||||
@@ -15,6 +15,7 @@ import { DeleteConfirmDialog } from "./DeleteConfirmDialog";
|
||||
import { StopConfirmDialog } from "./StopConfirmDialog";
|
||||
import { SteerInput, ComposerFooter } from "./SteerInput";
|
||||
import { WorkflowBar } from "./WorkflowBar";
|
||||
import { DatabaseManagementPage } from "./DatabaseManagementPage";
|
||||
import { Pencil, ArrowLeft, ArrowRight, Trash2 } from "lucide-react";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Checkbox } from "../components/ui/checkbox";
|
||||
@@ -39,6 +40,8 @@ interface AppShellProps {
|
||||
canLogout: boolean;
|
||||
}
|
||||
|
||||
type ActiveSurface = "core" | "database-management";
|
||||
|
||||
export function AppShell({ canLogout }: AppShellProps) {
|
||||
const authenticatedUser = useAuthUser();
|
||||
const [panelSession, setPanelSession] = useState<SessionSummary | null>(null);
|
||||
@@ -118,6 +121,7 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const [showActivity, setShowActivity] = useState(false);
|
||||
const [activeSurface, setActiveSurface] = useState<ActiveSurface>("core");
|
||||
const [workspaceManagerOpen, setWorkspaceManagerOpen] = useState(false);
|
||||
const [piManagementOpen, setPiManagementOpen] = useState(false);
|
||||
const [activeOpen, setActiveOpen] = useState(true);
|
||||
@@ -195,6 +199,7 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
function openPanel(id: string) {
|
||||
const s = sessions.find((x) => x.id === id);
|
||||
if (!s) return;
|
||||
setActiveSurface("core");
|
||||
// A session with a live Pi runtime opens straight into its live view: doResume
|
||||
// reconnects to the already-active runtime and replays its pending gate, so an
|
||||
// in-progress session never shows an empty screen that reads as "stopped". Cold or
|
||||
@@ -214,6 +219,7 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
});
|
||||
}
|
||||
async function doResume(id: string) {
|
||||
setActiveSurface("core");
|
||||
const guard = captureAuthOperation({ sessionId: id, disposalEpoch: operationEpochRef.current });
|
||||
if (!guard) return;
|
||||
const token = ++resumeInvocationRef.current;
|
||||
@@ -495,6 +501,7 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
}, [lastSystemEvent]);
|
||||
|
||||
function startNewSession() {
|
||||
setActiveSurface("core");
|
||||
invalidateResumeIntent();
|
||||
newSessionOperationRef.current = null;
|
||||
resetSession();
|
||||
@@ -580,9 +587,10 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
onClose={() => setPanelSession(null)}
|
||||
onResume={doResume}
|
||||
desktopSplit={sessionDesktopSplit}
|
||||
hidden={activeSurface !== "core"}
|
||||
/>
|
||||
)}
|
||||
{panelSession && sessionDesktopSplit && (
|
||||
{activeSurface === "core" && panelSession && sessionDesktopSplit && (
|
||||
<div className="relative w-0 shrink-0">
|
||||
<div
|
||||
{...sessionSeparatorProps}
|
||||
@@ -594,8 +602,8 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{showActivity && <ModelActivityPanel desktopSplit={desktopSplit} onClose={() => setShowActivity(false)} onOpenWorkspaceManager={() => setWorkspaceManagerOpen(true)} />}
|
||||
{showActivity && desktopSplit && (
|
||||
{showActivity && <ModelActivityPanel desktopSplit={desktopSplit} onClose={() => setShowActivity(false)} onOpenWorkspaceManager={() => setWorkspaceManagerOpen(true)} hidden={activeSurface !== "core"} />}
|
||||
{activeSurface === "core" && showActivity && desktopSplit && (
|
||||
<div className="relative w-0 shrink-0">
|
||||
<div
|
||||
{...activitySeparatorProps}
|
||||
@@ -610,6 +618,11 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
|
||||
{/* Conversation column */}
|
||||
<div data-testid="conversation-column" className="flex min-w-0 flex-1 flex-col">
|
||||
{activeSurface === "database-management" && <DatabaseManagementPage />}
|
||||
<div
|
||||
aria-hidden={activeSurface !== "core"}
|
||||
className={activeSurface === "core" ? "contents" : "hidden"}
|
||||
>
|
||||
{activeSessionId && (
|
||||
<div className="relative shrink-0 border-b border-border/70 bg-background/80 px-6 py-2.5 backdrop-blur">
|
||||
{/* Arrow toggle for the left Model-activity panel: → opens it, ← closes it. */}
|
||||
@@ -686,6 +699,7 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right session rail */}
|
||||
@@ -731,6 +745,22 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
>
|
||||
Workspace management
|
||||
</Button>
|
||||
{canManageWorkspace && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={[
|
||||
"w-full justify-start hover:bg-sidebar-accent",
|
||||
activeSurface === "database-management"
|
||||
? "bg-sidebar-accent font-bold text-sidebar-accent-foreground"
|
||||
: "",
|
||||
].join(" ")}
|
||||
aria-current={activeSurface === "database-management" ? "page" : undefined}
|
||||
onClick={() => setActiveSurface("database-management")}
|
||||
>
|
||||
Database management
|
||||
</Button>
|
||||
)}
|
||||
{canManagePi && (
|
||||
<Button
|
||||
variant="outline"
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export function DatabaseManagementPage() {
|
||||
return (
|
||||
<main
|
||||
aria-label="Database management"
|
||||
className="flex-1 overflow-y-auto bg-background"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -62,10 +62,12 @@ export function ModelActivityPanel({
|
||||
desktopSplit = false,
|
||||
onClose,
|
||||
onOpenWorkspaceManager,
|
||||
hidden = false,
|
||||
}: {
|
||||
desktopSplit?: boolean;
|
||||
onClose: () => void;
|
||||
onOpenWorkspaceManager?: () => void;
|
||||
hidden?: boolean;
|
||||
}) {
|
||||
const activityLog = useSessionStore((s) => s.activityLog);
|
||||
const visibleActivity = activityLog.filter(isVisibleModelActivity);
|
||||
@@ -80,7 +82,7 @@ export function ModelActivityPanel({
|
||||
}, [visibleCount, visibleTail]);
|
||||
|
||||
return (
|
||||
<aside className={desktopSplit
|
||||
<aside aria-hidden={hidden || undefined} hidden={hidden} className={desktopSplit
|
||||
? "static z-auto flex min-w-0 w-[var(--activity-panel-width)] shrink-0 flex-col border-r-0 bg-sidebar"
|
||||
: "fixed inset-y-0 left-0 z-30 flex min-w-0 w-[min(90vw,24rem)] shrink-0 flex-col border-r border-border bg-sidebar"
|
||||
}>
|
||||
|
||||
@@ -13,6 +13,7 @@ interface Props {
|
||||
onClose: () => void;
|
||||
onResume: (id: string) => void;
|
||||
desktopSplit?: boolean;
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
function statusLabel(s: SessionSummary): string {
|
||||
@@ -150,7 +151,7 @@ function decisionChipClass(type: string): string {
|
||||
return "bg-[oklch(var(--primary)/0.1)] text-primary";
|
||||
}
|
||||
|
||||
export function SessionDocumentsPanel({ session, onClose, onResume, desktopSplit = false }: Props) {
|
||||
export function SessionDocumentsPanel({ session, onClose, onResume, desktopSplit = false, hidden = false }: Props) {
|
||||
const { data: docs = [], isLoading } = useQuery<SessionDocument[]>({
|
||||
queryKey: ["session-documents", session.id],
|
||||
queryFn: () => getSessionDocuments(session.id),
|
||||
@@ -160,6 +161,8 @@ export function SessionDocumentsPanel({ session, onClose, onResume, desktopSplit
|
||||
return (
|
||||
<aside
|
||||
aria-label="Session summary"
|
||||
aria-hidden={hidden || undefined}
|
||||
hidden={hidden}
|
||||
className={[
|
||||
"flex shrink-0 flex-col border-r border-border bg-sidebar",
|
||||
desktopSplit
|
||||
|
||||
Reference in New Issue
Block a user