chore: init ThothII repo — gitignore references, baseline docs (PRD, spec, harness plan)
This commit is contained in:
+45
@@ -0,0 +1,45 @@
|
||||
# === macOS ===
|
||||
.DS_Store
|
||||
|
||||
# === Reference / consultation material (local-only, NOT ThothII deliverables) ===
|
||||
ChironeWp3/
|
||||
Thoth/
|
||||
|
||||
# === Visual companion brainstorming artifacts (local-only) ===
|
||||
.superpowers/
|
||||
|
||||
# === Python ===
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
.venv/
|
||||
venv/
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
|
||||
# === Node ===
|
||||
node_modules/
|
||||
|
||||
# === Secrets — NEVER commit ===
|
||||
.env
|
||||
*.pem
|
||||
ca-chain.pem
|
||||
config/ca-chain.pem
|
||||
|
||||
# === Runtime data (sessions contain PII; indexes are derived) ===
|
||||
harness/sessions/
|
||||
harness/indexes/
|
||||
backend/sessions/
|
||||
backend/indexes/
|
||||
|
||||
# === Test artifacts ===
|
||||
.pytest_cache/
|
||||
.ruff_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
# === Editor ===
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "https://app.kilo.ai/config.json",
|
||||
"indexing": {
|
||||
"vectorStore": "qdrant",
|
||||
"model": "sentence-transformers/all-minilm-l12-v2"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,761 @@
|
||||
# ThothII — Design dell'architettura
|
||||
|
||||
**Data:** 2026-06-25
|
||||
**Stato:** Draft, in attesa di review
|
||||
**Fonti:** `prd/ThothII-prd.md`, analisi di `ChironeWp3/` (harness funzionante) e `Thoth/thoth_sqldb2/` (modulo DB di riferimento)
|
||||
|
||||
---
|
||||
|
||||
## 1. Obiettivo
|
||||
|
||||
Costruire un sistema che, a partire da una richiesta in linguaggio naturale, generi SQL eseguibile su un database target, attraverso un workflow human-in-the-loop a 8 fasi orchestrato dal coding harness Pi in modalità RPC. Il sistema sostituisce l'interazione terminale di ChironeWp3 con un'interfaccia React guidata da uno scambio strutturato di JSON.
|
||||
|
||||
ThothII si articola in **tre progetti autonomi** (harness, backend, frontend), sviluppati e testabili in modo indipendente, integrati tramite un contratto JSON esplicito.
|
||||
|
||||
### Posizione su ChironeWp3 (premessa importante)
|
||||
|
||||
ChironeWp3 è il **punto di partenza** dell'harness di ThothII, **non un asset intoccabile o "collaudato al 100%"**. Il suo codice (CLI `nsp` in Python, gate extension in JS, skills markdown, modelli di sessione/workflow) viene **portato dentro il progetto `harness/` di ThothII per essere rivalidato e perfezionato**, non assunto come affidabile per inerzia. Nello sviluppo si applicano quindi, per ogni componente portata:
|
||||
|
||||
- **Lettura critica** del codice portato: si verifica che faccia davvero ciò che lo spec descrive, si individuano rigidità, duplicazioni (come il drift `PHASE_NAMES` già scoperto tra Python e JS), accoppiamenti nascosti, e invarianti sottintesi (come il no-limbo enforcement solo in JS).
|
||||
- **Aggiornamento** dove ThothII cambia il contratto: il gate passa da TUI a widget-descriptor (D2/D4); `phase.py` passa da ladder `if==N` a `workflow.yaml` data-driven (F2); il modello vector DB passa a doppia key (D11); si aggiunge `nsp memory save-one`. Queste **non sono riusi passivi**: sono modifiche che vanno progettate e testate.
|
||||
- **Copertura di test** (D10): i golden test su widget-descriptor validano il comportamento portato, anche per le parti "ereditate". Nessun componente viene considerato pronto solo perché proveniva da ChironeWp3.
|
||||
|
||||
Dove lo spec dice "riuso" va inteso come "**punto di partenza da adattare e validare**", non come "codice sicuro da prendere tal quale". Le decisioni che scelgono il riuso lo fanno perché **riducono il rischio rispetto a una riscrittura da zero** — ma il riuso stesso è lavoro di adattamento, non un'assunzione di affidabilità.
|
||||
|
||||
---
|
||||
|
||||
## 2. Decisioni architetturali ( locked )
|
||||
|
||||
Le decisioni seguenti sono state prese durante il brainstorming. Ogni voce riporta l'opzione scelta e il perché.
|
||||
|
||||
**D1 — Decomposizione: tre progetti autonomi, harness autosufficiente**
|
||||
`harness/` contiene tutto il layer Pi: skills markdown, `nsp` CLI Python (codice deterministico), gate extension JS, `.pi/`. `backend/` è puro Node+Fastify. `frontend/` è React/Next/ShadCn/AGGrid.
|
||||
Perché: punto di partenza ampio da ChironeWp3 (da rivalidare e adattare in `harness/`, non assunto affidabile per inerzia — vedi §1), confini puliti, l'harness resta testabile in isolamento scambiando JSON.
|
||||
|
||||
**D2 — Contratto centrale: widget-descriptor JSON**
|
||||
L'harness emette e riceve messaggi JSON strutturati (vedi §4) invece di un TUI. Il backend è un traduttore passivo che forwarda questi messaggi tra Pi e frontend.
|
||||
Perché: mappa 1:1 i tipi di interazione del PRD, è minimale e testabile.
|
||||
|
||||
**D3 — Workspace come YAML**
|
||||
I workspace (DB relazionale + pgvector + evidence + embeddings) sono definiti in `harness/workspaces/<name>.yaml`. I secret stanno in `.env`, referenziati come `${VAR}`. Il caricamento è isolato nel modulo `harness/nsp/workspace.py`.
|
||||
Perché: coerente con ChironeWp3, versionabile, testabile; il modulo `workspace.py` è il confine per future migrazioni.
|
||||
|
||||
**D4 — Gate come extension JS dentro Pi**
|
||||
La logica del gate (anti-bypass, input-lock, presentazione dei widget, iniezione kickoff) resta in un'extension JS dentro Pi, adattata per emettere widget-descriptor via `extension_ui_request`/`extension_ui_response`. Il backend non implementa gate.
|
||||
Perché: la logica del gate è la parte più delicata di ChironeWp3 (anti-bypass, no-limbo, iniezione kickoff) ed è il punto di partenza più ragionevole — nonostante richieda rivalutazione e adattamento al nuovo contratto widget-descriptor (vedi §1). Il protocollo RPC di Pi è progettato per questo. Spostarla nel backend significherebbe reimplementare l'anti-bypass da zero.
|
||||
|
||||
**D5 — Persistenza su filesystem (identica a ChironeWp3)**
|
||||
Sessioni e artefatti su `harness/sessions/<id>/`. Il ledger delle decisioni (`review_decisions.jsonl`) è append-only ed è la verità. La fase corrente è derivata (chronological fold del ledger). Il backend fa da proxy REST verso il filesystem via `nsp ... --json`.
|
||||
Perché: il modello di sessione di ChironeWp3 (ledger append-only + fold cronologico) è uno dei pezzi di valore e il punto di partenza più solido — da rivalutare e adattare (es. introducendo `schema_version`, §5.3) ma non da riscrivere da zero nel backend.
|
||||
|
||||
**D6 — Auth a livelli con middleware OIDC pluggabile**
|
||||
Tre modalità selezionate da config: `none` (utente `dev@local`, **modalità primaria nell'MVP modello B** — l'operatore è sulla propria macchina), `mock` (utente statico da header per test), `oidc` (OIDC standard: Authentik, Entra ID — stessa codepath, rilevante nell'evoluzione ad A "web app centrale"). Le sessioni ThothII sono associate all'utente autenticato (campo `author` nel manifest).
|
||||
Perché: copre tutti i casi del PRD con una sola codepath OIDC; `none` è la scelta naturale per l'MVP modello B (postazione singolo-operatore) e rende i test dell'harness indipendenti dall'auth.
|
||||
|
||||
**D7 — Backend Node+Fastify+TS con DB ibrido**
|
||||
`nsp` Python possiede tutta la logica DB deterministica (introspection, value sampling, RRF/LSH, schema-link, validazione/preview durante il workflow). Il backend Node si collega al DB del workspace solo per eseguire lo SQL finale approvato in read-only, alimentare AGGrid e gli export.
|
||||
Perché: coerente con D1 (harness autosufficiente); risolve informix (driver Node assenti); nessuna riscrittura di thoth_sqldb2 in TS.
|
||||
**Rischio architetturale (D7):** due codepath di esecuzione SQL vanno mantenute allineate sull'enforcement read-only. Mitigazione: un'unica fonte di verità per "cosa è permesso eseguire" (config `execution.allow` del workspace, condivisa tra `nsp` e backend).
|
||||
|
||||
**D8 — `nsp` CLI resta Python**
|
||||
Riuso diretto di ChironeWp3 (SQLAlchemy, psycopg2, datasketch/LSH, RRF, schema-link, output `--json`). Il gate JS dentro Pi lo chiama via `bash`.
|
||||
Perché: partire dal codice Python esistente di ChironeWp3 (SQLAlchemy, psycopg2, datasketch/LSH, RRF, schema-link) riduce il rischio rispetto a una riscrittura TS da zero — ma il codice va comunque portato in `harness/`, letto criticamente, adattato ai nuovi contratti e coperto dai golden test (D10). I "due runtime" nell'harness non sono un problema reale.
|
||||
|
||||
**D9 — Strategia di implementazione: vertical slice per fase del workflow**
|
||||
Ordine harness → backend → frontend, ma con loop end-to-end precoci: ad ogni ciclo di sviluppo si estendono tutti e tre i layer solo per la **fase del workflow** corrente (F1, poi F2, ecc. — non "fasi di sviluppo" generiche). Feedback continuo, nessun progetto in sospeso.
|
||||
|
||||
**D10 — Test dell'harness: fake-Pi JSONL + golden test**
|
||||
Un `fake-pi` (script Python o JS) implementa il protocollo RPC di Pi. I test verificano che i widget-descriptor emessi matchino golden JSON salvati. Deterministico, senza LLM né Pi reale nei CI.
|
||||
Perché: è l'unica che testa il contratto widget-descriptor (la parte nuova) in modo deterministico e ripetibile.
|
||||
|
||||
**D11 — Modello a doppia API key per il vector DB + upsert mirato delle Memory**
|
||||
Il vector DB (pgvector) è esposto via REST con **due endpoint separati, due API key distinte**:
|
||||
- **Reader** (`vector_rest`, key `*_API_KEY`): solo `search_similar` / `list_tables`, sola lettura.
|
||||
- **Writer** (`vector_write_rest`, key `*_WRITE_API_KEY`, opzionale): solo `existing_vector_hashes` / `upsert_vector_records`, upsert + hash sync, **no delete/clear**.
|
||||
|
||||
Entrambi condividono la stessa URL e lo stesso tipo di config (`RestConfig | None`). "Writer assente" = sezione omessa o key vuota. Un unico client HTTP (`VectorRestClient`) parametrizzato dalla config: quale `RestConfig` gli viene passata determina key e allowlist.
|
||||
|
||||
Per salvare una Memory generata durante una sessione (anche remota), ThothII introduce un **nuovo comando mirato `nsp memory save-one <decision_seq>`** (deviazione controllata da ChironeWp3, dove il salvataggio avviene solo via `memory index` = resync completo, server-only). `save-one` fa un singolo upsert di un record via writer key, usando le RPC `upsert_vector_records` esistenti, con hash dedup client-side (SHA-256 del content, embedda solo new/changed). È gated da `require_vector_write_allowed` (permesso su workstation **solo se** writer configurato, exit 4 altrimenti).
|
||||
Perché: il caso d'uso reale è "salvare la memory appena creata in F5" — un resync intero (`memory index`) è sovradimensionato e `memory promote` è server-only. L'upsert mirato è efficiente e abilita il lavoro remoto (scopo esplicito della doppia key). Riuso totale delle RPC writer e del pattern di gating di ChironeWp3; solo il comando `save-one` è nuovo.
|
||||
|
||||
**D12 — Modello di deployment: postazione remota (B) nell'MVP, evoluzione possibile a web app centrale (A)**
|
||||
L'MVP segue il modello "postazione remota" di ChironeWp3: **tutti e tre i layer girano sulla macchina dell'operatore** in localhost (pattern "app desktop con UI browser", come Jupyter o VS Code server). Il vector DB e il DWH restano centrali, raggiungiti via REST con la doppia key (D11). L'evoluzione futura a "web app centrale" (A) sposta backend+harness su un server centrale e serve il frontend via browser a più utenti; i contratti FE↔BE e interni **non cambiano** — è un riposizionamento di deployment, non una riscrittura.
|
||||
Perché: corrisponde al modo di lavoro reale attuale (operatori su postazioni dedicate fuori dal server). Mantenere i 3 layer anche in B rende l'evoluzione B→A pulita (nessun refactor dei contratti) e lascia all'harness le sue responsabilità (workflow, gate) e al backend le sue (REST/SSE, job, API stabile) senza mescolarle.
|
||||
|
||||
**D13 — Il testo libero dell'utente va interpretato, non ignorato**
|
||||
Ogni volta che una risposta permette testo libero (campo `freetext` del widget, opzione "Altro — specifica", motivazione di un rifiuto, steering `!`), l'harness **deve valutare il testo dell'utente cercando di interpretarlo al meglio nel contesto corrente della sessione** (domanda, fase, artefatto mostrato, decisioni già prese), invece di passare alla risposta di default. Questa è una **deviazione comportamentale esplicita** da ChironeWp3, che tende a ignorare il testo libero a favore della risposta di default.
|
||||
Perché: il revisore che si prende la briga di scrivere testo libero sta comunicando qualcosa che le opzioni predefinite non coprono. Ignorarlo degrada la qualità del risultato (la sua correzione va persa) e la fiducia nell'interazione. Il costo è nel prompt/gate, non in una nuova infrastruttura. Vedi §4.6 per il comportamento atteso.
|
||||
|
||||
**D14 — Gestione esplicita delle richieste incomprensibili (non ambigue): Value/Schema Linking e SQL Formula Evidence**
|
||||
ChironeWp3 tratta due casi critici in modo inadeguato e vanno **sviluppati esplicitamente** in ThothII come capacità di prima classe dell'harness. Entrambi riguardano la situazione in cui la richiesta dell'utente **non è ambigua** (il revisore sa cosa vuole) ma è **incomprensibile per il modello** senza chiarimenti o evidenze. Vedi §4.7 per il dettaglio dei due casi.
|
||||
Perché: sono i casi in cui un NL→SQL "silenzioso" produce SQL sbagliato senza che nessuno se ne accorga (il modello indovina la colonna sbagliata per un valore, o inventa una formula per un concetto). Svilupparli è core-value, non optional. Sono punti di **perfezionamento sostanziale** del codice portato da ChironeWp3 (vedi §1).
|
||||
|
||||
**D14a — Value and Schema Linking (value → column grounding).** Quando la domanda cita un valore (es. "ablazione", "DRG 123", "fibrillazione atriale") il cui mapping a colonna/e è poco chiaro per il modello, l'harness deve **chiarire con l'utente** (usando l'indice LSH + RRF, che già restituiscono `table.column → valore → score`) quale colonna/e corrispondono al valore — gestendo esplicitamente il caso multi-colonna e quello in cui il valore richiede una formula (si collega a D14b). In ChironeWp3 la metà retrieval esiste (`nsp search --kind values`) ma manca del tutto la metà workflow: nessun decision type, nessuna istruzione skill, nessun widget, l'aggregazione LSH collassa un valore presente in N colonne a una sola.
|
||||
Perché: è il caso in cui il modello scrive `WHERE colonna_sbagliata = 'ablazione'` in silenzio. Il revisore che cita un valore lo fa apposta — va confermato il grounding prima che diventi SQL.
|
||||
|
||||
**D14b — SQL functions and formula evidence (concept → formula, reviewer-approved).** Quando la domanda contiene un concetto calcolato (es. "fascia di età pediatrica", "indice di Charlson", "ricovero a 30 giorni", ma anche "ablazione" quando richiede più colonne) che si traduce in una formula SQL su più campi, l'harness deve **proporre la formula e chiedere l'approvazione del revisore** prima che fluisca nel CTE/SQL. Richiede un nuovo tipo di evidenza "formula" (il `tier:"concept"` di ChironeWp3 è definito ma **mai usato**; il contenuto esiste già negli `30-esempi-nlq/*.md` ma come prose, non come unità recuperabile/validabile) e un flusso di approvazione per-concetto (analogico al gate CTE di F6, ma a granularità concetto e potenzialmente riutilizzabile tra sessioni).
|
||||
Perché: oggi il modello inventa la formula o la legge da prose non strutturata, senza conferma. Un concetto calcolato tradotto male invalida tutta la query. L'approvazione del revisore sul SQL-espressione + colonna/e è load-bearing per la correttezza.
|
||||
|
||||
**D15 — Rollback a tre granularità con teardown completo dei documenti**
|
||||
Il revisore deve poter tornare indietro a tre livelli: (a) **ripresenta il widget corrente e scarta l'ultima risposta** (granularità step, dentro la stessa fase); (b) **torna all'inizio dello step precedente del workflow** (fase precedente); (c) **torna a uno step specifico** (qualsiasi fase precedente). In ogni caso di rollback, **tutte le scelte fatte dopo il punto di rollback vanno dimenticate e gli artefatti prodotti vanno cancellati**. Questa è una **deviazione sostanziale** da ChironeWp3, dove `phase reopen` fa solo `append_decision` (niente teardown), gli helper aggregano decisioni stale+nuove ignorando il boundary di reopen, e non esiste la granularità step. Vedi §4.8.
|
||||
Perché: un rollback che lascia artefatti stale e decisioni incoerenti è peggio di niente — il modello/prossimo passo legge uno stato inconsistente (es. CTE orfani che bloccano `finalize`, già verificato come bug latente in ChironeWp3). La correttezza post-rollback è load-bearing.
|
||||
|
||||
**D16 — Minimizzazione del contesto per LLM medio (35B, <200k token) tramite task document per-step**
|
||||
L'architettura deve far sì che ad ogni passaggio il modello riceva **un singolo documento di task con esattamente le informazioni necessarie per eseguire il task corrente, derivate dagli step precedenti** — non l'intero contesto accumulato nella conversazione. Obiettivo: poter usare un modello di medie dimensioni (35B param, contesto <200k). L'implementazione richiede: (1) un generatore di **task document** che legge gli artefatti precedenti e emette il slice minimale per la fase corrente; (2) **enforcement** che il modello non legga mai artefatti integrali fatali (es. `physical.yaml` = 760KB ≈ 190k token, `report.md` = 344KB — entrambi fatali per un 35B); (3) gestione del contesto della chat (pruning/ricomposizione) perché la conversazione non cresca senza bound. Vedi §4.9.
|
||||
Perché: la base "context-fresh dagli artefatti via `nsp`" di ChironeWp3 è giusta, ma non compatta il contesto e non enforce i bound — un modello 35B collasserebbe leggendo lo schema fisico integrale. La target hardware impone il constraint; il task document per-step è la soluzione architetturale.
|
||||
|
||||
---
|
||||
|
||||
## 3. Architettura dei tre progetti e flusso dei dati
|
||||
|
||||
**Modello di deployment (D12, MVP = B "postazione remota"):** i tre layer girano in localhost sulla macchina dell'operatore. Il vector DB e il DWH restano centrali, raggiungibili via REST con la doppia key (D11). L'evoluzione futura ad A ("web app centrale") riposiziona backend+harness su server centrale senza modificare i contratti.
|
||||
|
||||
```
|
||||
┌─ POSTAZIONE OPERATORE (localhost, MVP) ─────────────────────────────┐
|
||||
│ │
|
||||
│ ┌─ FRONTEND (frontend/) ─────────────────────────────────────────┐ │
|
||||
│ │ browser → localhost · React + Next.js + ShadCn + AGGrid │ │
|
||||
│ │ Consuma SOLO la REST del backend (in localhost). │ │
|
||||
│ │ • SSE: stream di eventi (text_delta, ui_request, lifecycle) │ │
|
||||
│ │ • POST: risposte utente, azioni (reset fase, export) │ │
|
||||
│ └───────────────────────────────▲──────────────────────────────────┘ │
|
||||
│ │ HTTP/SSE (JSON, localhost) │
|
||||
│ ┌─ BACKEND (backend/) ───────────┴────────────────────────────────┐ │
|
||||
│ │ Node.js + Fastify + TypeScript · gira in localhost │ │
|
||||
│ │ • Avvia Pi: spawn("pi", ["--mode","rpc"], {cwd: repoRoot}) │ │
|
||||
│ │ • RpcClient: LineSplitter (LF only!) + dispatch per id │ │
|
||||
│ │ • Traduttore: Pi extension_ui_request → ui_request (FE) │ │
|
||||
│ │ FE ui_response → extension_ui_response (Pi) │ │
|
||||
│ │ • DB workspace: SOLO SQL finale read-only → AGGrid/export │ │
|
||||
│ │ • REST: workspaces, sessions, workflow, artifacts │ │
|
||||
│ │ • Auth middleware (D6): none (primaria MVP B) | mock | oidc │ │
|
||||
│ └───────────────────────────────▲──────────────────────────────────┘ │
|
||||
│ │ JSONL (newline-delimited, LF only)│
|
||||
│ ┌─ HARNESS (harness/) ───────────┴────────────────────────────────┐ │
|
||||
│ │ ├── .pi/ ← config progetto Pi │ │
|
||||
│ │ │ ├── settings.json, themes/ │ │
|
||||
│ │ │ ├── prompts/ ← /nuova-domanda, /riprendi │ │
|
||||
│ │ │ ├── skills/nsp-sessione/ ← SKILL.md + rewriting/mem/cte/sql│ │
|
||||
│ │ │ └── extensions/nsp-gate.js ← GATE: anti-bypass + widget │ │
|
||||
│ │ ├── nsp/ (Python package) ← CLI deterministica, parla --json│ │
|
||||
│ │ │ ├── cli/ ← command groups (da ChironeWp3) │ │
|
||||
│ │ │ ├── workspace.py ← caricamento YAML (confine D3) │ │
|
||||
│ │ │ ├── workflow.py ← lettura workflow.yaml (F2) │ │
|
||||
│ │ │ ├── db/, rest/, search/, mschema/, vectorstore/, session/ │ │
|
||||
│ │ ├── workflow.yaml ← UNICA definizione workflow (F2) │ │
|
||||
│ │ ├── workspaces/*.yaml ← definizioni workspace │ │
|
||||
│ │ ├── sessions/<id>/ ← persistenza FS (locale, PII) │ │
|
||||
│ │ └── tests/ + fake-pi/ ← golden test (D10) │ │
|
||||
│ └──────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└────────────────────────────────▲─────────────────────────────────────┘
|
||||
│ HTTPS (443), doppia API key (D11)
|
||||
┌─ SERVER / SUPABASE CENTRALE ────┴─────────────────────────────────────┐
|
||||
│ /dwh/ PostgREST ──► DWH (schema datawarehouse, read-only) │
|
||||
│ /vector/v1 RPC allowlist │
|
||||
│ ├── reader (key reader): search_similar, list_tables │
|
||||
│ └── writer (key writer): existing_vector_hashes, │
|
||||
│ upsert_vector_records (no del)│
|
||||
│ ──► pgvector (schema_records/evidence/ │
|
||||
│ memory) │
|
||||
└──────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Flusso di una decisione (es. "promuovi tabella" in F4)
|
||||
|
||||
1. Il LLM dentro Pi chiama il tool `reviewer_decide`. Il gate `nsp-gate.js` costruisce un widget-descriptor `{type:"ui_request", id:"u42", phase:"F4", widget:"multiselect", options:[...]}`.
|
||||
2. Il gate lo emette via `ctx.ui.custom`. In RPC mode diventa `extension_ui_request`.
|
||||
3. Il RpcClient del backend lo riceve, lo forwarda via SSE al frontend come `ui_request`.
|
||||
4. Il frontend renderizza il widget, l'utente seleziona, il FE fa `POST /sessions/:id/response` con `{type:"ui_response", id:"u42", choices:[...], decision:{type:"table_promoted"}}`.
|
||||
5. Il backend lo traduce in `extension_ui_response` e lo scrive su stdin di Pi.
|
||||
6. Il gate lo legge, chiama `nsp decision add` (autorizzato dal suo stesso hook anti-bypass), il ledger si aggiorna, la fase deriva.
|
||||
|
||||
---
|
||||
|
||||
## 4. Il contratto widget-descriptor (D2)
|
||||
|
||||
Questa è la parte centrale: il "linguaggio" tra harness, backend e frontend. Deriva dalla verifica esaustiva dei pattern di interazione reali di ChironeWp3 (10 pattern, 4 primitive UI), mappati su una tassonomia di 6 widget.
|
||||
|
||||
### Principi di flessibilità
|
||||
|
||||
Il contratto è progettato per accogliere future modalità di interazione:
|
||||
|
||||
- **`widget` è un campo aperto, non un enum chiuso.** I 6 widget sono i `kind` iniziali registrati. Aggiungerne uno richiede definire un nuovo `kind`, il suo payload e il renderer frontend. Non cambia l'infrastruttura (forwarding, correlazione `id`, ledger).
|
||||
- **Estensibilità per composizione, non per enumerazione.** I comportamenti complessi si ottengono componendo primitive (artefatto+decisione = `artifact-gate`; select+testo se Altro = linkage `option.opens`; multiselect+contesto = `multiselect` con `content`).
|
||||
- **Versioning del contratto + fallback graceful.** Ogni messaggio porta `schema_version`. Il frontend gestisce i `kind` sconosciuti con un fallback universale: se arriva un widget che non sa renderizzare, mostra il payload come JSON formattato in un box "Widget non supportato (kind: X) — rispondi manualmente".
|
||||
|
||||
### Messaggi harness → backend → frontend
|
||||
|
||||
```jsonc
|
||||
// ui_request — l'harness chiede qualcosa all'utente
|
||||
{
|
||||
"type": "ui_request",
|
||||
"id": "u42", // correlazione con la risposta
|
||||
"schema_version": 1,
|
||||
"session_id": "2026-06-25-...",
|
||||
"phase": "F4_schema_linking", // fase del workflow
|
||||
"title": "Conferma le tabelle per lo schema-linking",
|
||||
"intro": "Ho selezionato 3 tabelle candidate. Segna quelle da promuovere.",
|
||||
"widget": "multiselect", // vedi tassonomia §4.1
|
||||
"options": [ // per select/multiselect
|
||||
{"id": "t_pazienti", "label": "pazienti",
|
||||
"meta": {"signals": {...}}, "selected": true}
|
||||
],
|
||||
"reserved": ["back", "exit", "other"], // opzioni di controllo framework
|
||||
"timeout_ms": null
|
||||
}
|
||||
|
||||
// info — messaggio informativo (tipo 1 PRD), non blocca, niente risposta
|
||||
{
|
||||
"type": "info",
|
||||
"schema_version": 1,
|
||||
"session_id": "...",
|
||||
"phase": "F4_schema_linking",
|
||||
"level": "info", // info|warning|error
|
||||
"text": "Sto per proporti lo schema-linking..."
|
||||
}
|
||||
```
|
||||
|
||||
### Messaggi frontend → backend → harness
|
||||
|
||||
```jsonc
|
||||
// ui_response — la risposta dell'utente a una ui_request
|
||||
{
|
||||
"type": "ui_response",
|
||||
"id": "u42", // correla la ui_request
|
||||
"kind": "multiselect",
|
||||
"choices": ["t_pazienti", "t_ricoveri"],
|
||||
"decision": {"type": "table_promoted"} // mappa sul ledger
|
||||
}
|
||||
|
||||
// reserved control — "torna indietro"/"esci"/"altro"
|
||||
{"type": "ui_response", "id": "u42", "control": "back"}
|
||||
{"type": "ui_response", "id": "u42", "control": "freetext", "text": "..."}
|
||||
```
|
||||
|
||||
### 4.1 Tassonomia dei widget (6)
|
||||
|
||||
Ogni voce: copertura, caratteristiche obbligatorie, esempio d'uso.
|
||||
|
||||
**`info`** — fire-and-forget (notifica toast, livello `info`/`warning`/`error`). Non blocca, non richiede risposta.
|
||||
Usata per: notifiche di sistema (es. "sto per proporti lo schema-linking"), preflight Ollama, warning di input-lock.
|
||||
|
||||
**`select`** — single-pick da una lista di opzioni.
|
||||
Caratteristiche obbligatorie: escape hatch framework iniettati (`Altro`/`Torna indietro`/`Esci`), marker `option.recommended` evidenziato "(consigliato)", linkage `option.opens` per aprire un widget figlio, invariante no-limbo (Esc/cancel non è mai risposta → loop).
|
||||
Usata per: disambiguazione F1, conferme sì/no (riapri fase), scelte singole.
|
||||
|
||||
**`multiselect`** — multi-pick con checkbox.
|
||||
Estensioni richieste: stato iniziale pre-selezionato (`selected[]`), toggle "seleziona/deseleziona tutti", `artifact.content` embed scrollabile (contesto), descrizione del focus (progressive disclosure), `allow_empty: true|false`, `Altro` inline che ritorna `{text, choices}` insieme, side-effect di deselezione (può emettere decision negativa).
|
||||
Usata per: promozione tabelle F4, selezione memoria F2/F5, evidence accepted/rejected.
|
||||
|
||||
**`freetext`** — testo libero. Sempre figlio di `select`/`artifact-gate` (via `Altro`/`Rifiuta`) o via canale steering ambientale. Il testo va **interpretato** dall'harness nel contesto della sessione, non ignorato a favore di una risposta di default (D13, vedi §4.6).
|
||||
Usata per: "Altro — specifica", motivazione del rifiuto, steering `!`.
|
||||
|
||||
**`artifact-gate`** — artefatto + lista disposizioni (fusione di artefatto e select del Pattern 3 di ChironeWp3). È il cuore decisionale di F5/F6/F7.
|
||||
Contenuto: artefatto renderizzato (schema_linking con render umano + JSON intero; cte che legge `ctes/<name>.sql`; sql che legge `sql_final.sql` + esegue preview live; memory, question, result) + disposizioni (`confirm`/`approve_reject`/`view_only`). `Rifiuta` → linkage a `freetext` per la motivazione.
|
||||
Perché separato da `artifact`: ChironeWp3 non mostra mai un artefatto puro — l'artefatto è sempre accoppiato a una decisione (Approva/Rifiuta/Altro/Torna/Esci). La decisione è load-bearing sul documento.
|
||||
|
||||
**`artifact`** — view-only, nessuna disposizione. Per artefatti da mostrare nel pannello destro senza decisione (COT, thinking, risultato in lettura).
|
||||
|
||||
### 4.2 Linkage annidato
|
||||
|
||||
Un'opzione può dichiarare quale widget apre se scelta:
|
||||
|
||||
```jsonc
|
||||
ui_request select:
|
||||
options: [
|
||||
{ id:"approve", label:"Approva" },
|
||||
{ id:"other", label:"Altro — specifica…",
|
||||
opens: { widget:"freetext", title:"Specifica…" } },
|
||||
{ id:"reject", label:"Rifiuta (rivedi e riprova)",
|
||||
opens: { widget:"freetext", title:"Motivazione del rifiuto" } },
|
||||
{ id:"back", label:"Torna indietro",
|
||||
opens: { widget:"select", title:"A quale fase?", options:[...] } }
|
||||
]
|
||||
```
|
||||
|
||||
Il frontend, se l'utente sceglie "other", mostra il widget figlio e raccoglie entrambe le risposte, inviandole insieme nella `ui_response`. Il testo raccolto via `Altro` o `Rifiuta` è testo libero che l'harness deve interpretare nel contesto (D13, §4.6), non un'etichetta da archiviare e dimenticare.
|
||||
|
||||
### 4.3 Canale steering ambientale
|
||||
|
||||
Free-text non modale (prefisso `!` in ChironeWp3): il revisore può iniettare testo arbitrario in qualsiasi momento durante una sessione attiva. Non è un widget modale, è un canale stream-level. Modellato come un endpoint `POST /sessions/:id/steer` con `{text}`. Lo steering è il caso più forte del principio D13: il revisore interrompe appositamente per ridirezionare — ignorare il testo o trattarlo come "continua con il default" vanifica lo scopo del canale.
|
||||
|
||||
### 4.4 Evento di sistema: auto-advance silenzioso
|
||||
|
||||
Una fase può chiudersi senza input utente (F2/F6 vuote, `phase_auto_approved`). Non è un widget — è l'assenza di interazione. Modellato come evento SSE distinto `{type:"system_event", event:"auto_advance_silent", phase:"F2"}` perché il revisore possa capire perché una fase è sparita.
|
||||
|
||||
### 4.5 Ledger decisioni (enumerati leggendo il codice ChironeWp3, 22 tipi)
|
||||
|
||||
I `decision.type` del widget-descriptor sono esattamente quelli del `review_decisions.jsonl` di ChironeWp3, verified leggendo `src/psdwp3/session/decisions.py:9-32`:
|
||||
|
||||
- **F1 chiarimento:** `concept_clarified`, `ambiguity_open`
|
||||
- **F2 memoria:** `memory_rejected` (side-effect di deselezione), `phase_auto_approved` (vuota)
|
||||
- **F3 riscrittura:** `question_rewritten`
|
||||
- **F4 schema-linking:** `table_promoted`, `table_excluded`, `column_corrected`, `join_modified`, `evidence_accepted`, `evidence_rejected`
|
||||
- **F5 sintesi:** `phase_approved`
|
||||
- **F6 cte:** `phase_skipped`, `cte_approved`, `cte_corrected`*, `cte_rejected`, `phase_auto_approved`
|
||||
- **F7 sql finale:** `sql_revised`, `sql_approved`, `sql_rejected`
|
||||
- **F8 datamart:** `datamart_requested`, `datamart_declined`
|
||||
- **meta cross-fase:** `phase_approved`, `phase_auto_approved`, `phase_reopened`, `phase_skipped`
|
||||
|
||||
(*) `cte_corrected` è nel `Literal` ma non è mai emesso dal gate di ChironeWp3 (reserved/historic).
|
||||
|
||||
### 4.6 Interpretazione del testo libero (D13)
|
||||
|
||||
Questa è una **deviazione comportamentale esplicita da ChironeWp3**, registrata come D13. Il principio: quando l'utente fornisce testo libero, sta comunicando qualcosa che le opzioni predefinite non coprono. L'harness lo valuta e cerca di interpretarlo nel contesto, invece di passare alla risposta di default.
|
||||
|
||||
**Dove si applica (tutti i canali di testo libero):**
|
||||
|
||||
- **`Altro — specifica…`** in `select` e `multiselect` (linkage `option.opens`, §4.2): l'utente descrive una scelta fuori dalle opzioni. L'harness interpreta il testo e, se necessario, lo traduce in una decisione o in una nuova proposta (es. in F1 disambiguazione, il testo può chiarire un concetto non previsto; in F4 può indicare una tabella o una correzione non in lista).
|
||||
- **`Rifiuta (rivedi e riprova)` → motivazione** (linkage, §4.2): la motivazione del rifiuto non è decorativa — deve guidare la rigenerazione. Rifiutare un CTE con "la join è sbagliata, va su dim_pazienti non fact_ricoveri" deve portare a rivedere proprio quel join, non a riproporre lo stesso CTE.
|
||||
- **Steering ambientale `!`** (§4.3): è il caso più forte. L'utente interrompe per ridirezionare ("stai escludendo i pazienti pediatrici", "considera solo il 2024"). Ignorarlo o trattarlo come "continua" vanifica lo scopo.
|
||||
|
||||
**Comportamento atteso dell'harness (il "cosa", non il "come"):**
|
||||
|
||||
1. **Riceve** il testo libero nella `ui_response` (campo `text` per Altro/motivazione, o payload dello steering).
|
||||
2. **Lo valuta nel contesto corrente**: domanda originale (e sua versione riscritta in F3+), fase attiva, artefatto mostrato, decisioni già presenti nel ledger, candidate/elementi in gioco. L'interpretazione è compito del LLM dentro Pi, guidato dalla skill; il gate si limita a consegnargli il testo e a non permettergli di "svignarsela" con un default.
|
||||
3. **Agisce coerentemente**: applica l'interpretazione — corregge la proposta, aggiunge un vincolo, riapre una fase se serve, riscrive la domanda se il chiarimento lo richiede. Se il testo è ambiguo, **chiede chiarimento** (emette un nuovo widget) invece di indovinare in silenzio o ignorare.
|
||||
4. **Lascia traccia**: quando il testo libero influenza una decisione, questa va registrata nel ledger con il `rationale` che riporta (anche sintetizzato) il testo dell'utente, così l'origine della decisione è ricostruibile.
|
||||
|
||||
**Cosa NON deve fare (anti-pattern da ChironeWp3 da evitare):**
|
||||
|
||||
- Non trattare il testo libero come etichetta opaca da archiviare e dimenticare.
|
||||
- Non passare alla risposta/default di default quando c'è testo non vuoto.
|
||||
- Non "accettare" la direzione dell'utente a parole e poi proseguire con la proposta originaria.
|
||||
- Non ignorare il testo dello steering trattandolo come conferma generica.
|
||||
|
||||
**Implementazione (dove vive):** la responsabilità è dell'harness — in particolare nella skill `nsp-sessione` (istruzioni al LLM su come trattare il testo libero in ogni fase) e nel gate `nsp-gate.js` (che consegna il testo al modello e, come per gli altri invarianti, non lascia spazio a "svignarsela"). Il backend e il frontend sono solo trasporto: il FE raccoglie il testo, il BE lo forwarda. Nessuna logica di interpretazione fuori dall'harness. È un punto esplicito di **perfezionamento del codice portato da ChironeWp3** (vedi §1), e va coperto dai golden test (D10) con scenari in cui l'utente fornisce testo libero e si verifica che l'harness ne tenga conto.
|
||||
|
||||
### 4.7 Gestione delle richieste incomprensibili (non ambigue) — Value/Schema Linking e SQL Formula Evidence (D14)
|
||||
|
||||
Questa sezione specifica il principio D14: due casi in cui la richiesta dell'utente **non è ambigua** (sa cosa vuole) ma è **incomprensibile per il modello** senza chiarimenti o evidenze. Sono i casi in cui un NL→SQL "silenzioso" produce SQL sbagliato senza allarme. Entrambi sono **sotto-sviluppati in ChironeWp3** e vanno sviluppati come capacità di prima classe. Sono punti di **perfezionamento sostanziale** del codice portato (vedi §1), non riuso passivo.
|
||||
|
||||
**Distinzione chiave — ambiguo vs incomprensibile.** F1 (chiarimento, `concept_clarified`/`ambiguity_open`) gestisce il caso *ambiguo*: il concetto della domanda ammette più letture e il modello chiede quale. I due casi qui sono *incomprensibili*: il modello non sa tradurre un elemento specifico della domanda in schema SQL, anche sapendo cosa vuole l'utente. Sono ortogonali a F1 e oggi cadono tra le fasi.
|
||||
|
||||
#### 4.7.1 Value and Schema Linking (D14a)
|
||||
|
||||
**Il caso.** La domanda cita un **valore** (es. "ablazione", "DRG 123", "fibrillazione atriale", "ricovero in UTIC") che il modello non sa mappare a una colonna. Il valore non è ambiguo (l'utente sa cosa intende), ma il modello non sa *dove vive* nello schema.
|
||||
|
||||
**Cosa esiste in ChironeWp3 (metà retrieval, OK).** L'indice LSH + RRF funziona: `nsp search "<valore>" --kind values --json` restituisce già `table.column → valore → score`. Il dato c'è.
|
||||
|
||||
**Cosa manca (metà workflow, da sviluppare).** Tutto il flusso che usa quel dato per chiarire con l'utente:
|
||||
- Nessun **decision type** per il value-grounding (es. `value_grounded`).
|
||||
- Nessuna **istruzione nella skill** di: "se la domanda cita un valore, esegui `nsp search --kind values`; se il valore mappa a >1 colonna o a una colonna non ovvia, chiedi conferma del grounding."
|
||||
- L'**aggregazione LSH collassa un valore presente in N colonne a una sola** (`_aggregate_lsh` in `search/__init__.py` tiene solo il best). Il caso multi-colonna non è rappresentato.
|
||||
- Nessun **widget dedicato** "conferma valore X → colonna Y".
|
||||
|
||||
**Il caso ablazione prova che il gap è reale e multi-colonna.** "Ablazione" nello schema Chirone si calcola da più punti: il flag `fact_..._ablazione.ablazione_transcatetere`, e/o `fact_see_ablazione_procedura_patologia.patologia = 'ablazione'`, e/o `procedure_type = 'ablazione'`. Il modello che scrive `WHERE ablazione_transcatetere IS TRUE` in silenzio può produrre una query semanticamente diversa da quella voluta. Il grounding del valore *è esso stesso* una decisione di formula (collegamento a §4.7.2).
|
||||
|
||||
**Comportamento atteso in ThothII:**
|
||||
|
||||
1. **Trigger:** durante la riscrittura/analisi (F3/F4), per ogni valore letterale citato nella domanda, l'harness esegue `nsp search "<valore>" --kind values`.
|
||||
2. **Decisione di chiarire:** se il valore mappa a **più di una colonna**, o a una colonna che il modello non avrebbe scelto da solo, o a una colonna che richiede formula (caso §4.7.2), l'harness **presenta un grounding clarification** (widget `select` o `multiselect` che mostra i candidati `colonna → valore → score` con provenance LSH).
|
||||
3. **Interpretazione del testo libero (D13):** se l'utente sceglie "Altro" e indica una colonna o una formula diversa, l'harness la valuta nel contesto, non la ignora.
|
||||
4. **Registrazione:** il grounding confermato si registra nel ledger con il nuovo decision type (es. `value_grounded`, `subject` = il valore, `detail` = colonna/e scelta, `rationale` = score + eventuale testo utente) e si riflette in `schema_linking.json` (i `Candidate` oggi non hanno un campo "valore grounded" / "valore di filtro").
|
||||
5. **Connessione alla formula:** se il grounding richiede una formula (es. ablazione), si passa al flusso §4.7.2.
|
||||
|
||||
**Nuovi elementi dati:**
|
||||
- Decision type `value_grounded` (+ `value_grounded_multi` se serve distinguere).
|
||||
- Estensione di `Candidate` in `schema_linking.json` con campo `grounded_values: [{value, column, score}]` per registrare dove ogni valore citato è stato ancorato.
|
||||
- Possibilmente un widget dedicato, ma il pattern `select`/`multiselect` con `meta` (provenance + score) basta; non serve un nuovo `kind` di widget.
|
||||
|
||||
**Dove nel workflow:**micro-step tra F3 (riscrittura) e F4 (schema linking), o un sotto-passo esplicito di F4. Nel modello data-driven di `workflow.yaml` (F2), si modella come un prerequisito/attività di una fase, non come fase numerica separata nell'MVP — ma l'hook va chiarito.
|
||||
|
||||
#### 4.7.2 SQL functions and formula evidence (D14b)
|
||||
|
||||
**Il caso.** La domanda contiene un **concetto calcolato** (es. "fascia di età pediatrica", "indice di Charlson", "ricovero a 30 giorni", "ablazione") che si traduce in una **formula SQL su più campi**. Il modello non può "indovinare" la formula: o la recupera da una evidenza di formula, o la sintetizza e la fa approvare.
|
||||
|
||||
**Cosa esiste in ChironeWp3 (contenuto, ma non strutturato).** Gli `artifacts/evidence/30-esempi-nlq/*.md` contengono **SQL completo già scritto** per concetti come ablazione, cardioversione, device. Ma è prose opaca dentro esempi domanda→SQL, non un'unità "concetto → formula" recuperabile e validabile. In particolare: `tier: "concept"` è definito nello schema evidence (`evidence/model.py`) ma **mai usato** — zero file lo impostano, la retrieval non lo filtra.
|
||||
|
||||
**Cosa manca (tutto il layer formula, da sviluppare).**
|
||||
- Nessun **tipo dato first-class "formula"**. Né evidence né le annotation di mschema possono esprimere "concetto C = espressione SQL E su colonne [c1, c2, …]".
|
||||
- Nessuna **retrieval per formula**: non esiste `nsp search --kind formula "<concetto>"`.
|
||||
- Nessun **decision type** `formula_approved`/`concept_formula_rejected`.
|
||||
- Nessuna **istruzione skill** di proporre la formula per un concetto e chiederne approvazione prima del CTE.
|
||||
- Nessun **flusso di approvazione per-concetto**: oggi si approvano CTE/query intere (F6), non formule per concetto. La parola "formula" non appare nel codice.
|
||||
|
||||
**Comportamento atteso in ThothII:**
|
||||
|
||||
1. **Authoring (offline):** un nuovo tipo di evidenza/artefatto per **formule di concetto** — frontmatter `{concept, columns[], sql, status (auto/draft/reviewed), sources}` + corpo SQL. Attiva il `tier:"concept"` morto (o un nuovo `kind`) rendendolo filtrato e recuperabile. Il contenuto esiste già negli `30-esempi-nlq`; va ristrutturato da "esempi domanda→SQL" a "unità concetto→formula".
|
||||
2. **Retrieval:** `nsp search --kind formula "<concetto>"` restituisce la/e formula/e candidata(e), con provenance (recuperata vs sintetizzata).
|
||||
3. **Runtime (per-sessione):** per ogni concetto calcolato nella domanda, l'harness **propone la formula** (recuperata o sintetizzata) e **chiede approvazione del revisore** sul SQL-espressione + colonna/e, *prima* che fluisca nel CTE/SQL. Il widget è `artifact-gate` con `kind:"formula"` (template pronto: il `runArtifactGate` di ChironeWp3 già mostra SQL scrollabile + approve/reject/other).
|
||||
4. **Registrazione:** decision type `concept_formula_approved`/`concept_formula_rejected`, con `subject` = concetto, `detail` = formula approvata, `rationale` = provenance + eventuale testo utente.
|
||||
5. **Riutilizzo (chiusura del cerchio con la memoria):** le formule approvate possono essere persistite nello store formule (come `memory.py` fa per le decisioni riusabili) così che un "indice di Charlson" approvato in una sessione venga riutilizzato nelle successive. Da considerare post-MVP.
|
||||
|
||||
**Nuovi elementi dati:**
|
||||
- Artefatto formula: `concept_formulas/<concept>.{yaml,sql}` (frontmatter + SQL) o un `kind:"formula"` in evidence con campo `sql`/`expression`.
|
||||
- Decision type `concept_formula_approved`, `concept_formula_rejected`.
|
||||
- Estensione di `schema_linking.json` con `concept_formulas: [{concept, sql, columns, status, source}]`.
|
||||
- `nsp search --kind formula` (parallelo a `--kind values` e `--kind schema`).
|
||||
- Opzionale: `nsp formula save-one` (analogico a `memory save-one`, D11) per persistere una formula approvata nello store centrale via writer key.
|
||||
|
||||
**Dove nel workflow:** micro-step di F4/F5 (dopo il grounding dei valori, prima della sintesi dello schema-linking). Nel modello `workflow.yaml` si modella come attività/prerequisito di una fase esistente, non come fase nuova nell'MVP.
|
||||
|
||||
#### 4.7.3 Relazione tra i due casi e con il workflow data-driven
|
||||
|
||||
I due casi sono **complementari e a volte sovrapposti**: "ablazione" è sia un problema di value-grounding (in quale colonna) sia di formula (booleano OR patologia OR tipo). Il design deve gestire il continuum:
|
||||
- grounding semplice (valore → 1 colonna, confermato),
|
||||
- grounding multi-colonna (valore → N colonne, selezionate),
|
||||
- grounding con formula (valore/concetto → formula SQL su più colonne, approvata).
|
||||
|
||||
Tutti e tre registrano una decisione tipizzata nel ledger e si riflettono in `schema_linking.json`, diventando parte dell'artefatto che F5 presenta per la conferma di sintesi. Essendo il workflow data-driven (F2, §5.3), **possono essere introdotti come micro-step senza rinumerare le fasi**: si aggiungono attività/prerequisiti alla fase F4 (o a una nuova F4.5 interna), e si evolve gradualmente.
|
||||
|
||||
**Impatto sui test (D10):** entrambi i casi vanno coperti da golden test — scenari in cui la domanda cita un valore multi-colonna (es. ablazione) e un concetto calcolato (es. fascia pediatrica), verificando che l'harness (a) presenti il grounding/formula gate e (b) registri la decisione corretta. Senza questi test il comportamento "silenzioso" di ChironeWp3 può riaffiorare.
|
||||
|
||||
### 4.8 Rollback a tre granularità con teardown (D15)
|
||||
|
||||
Questa sezione specifica D15. Il requisito: il revisore può tornare indietro a tre livelli, e in ogni caso le scelte fatte dopo il punto di rollback **vanno dimenticate** e gli artefatti prodotti **vanno cancellati**.
|
||||
|
||||
**Stato di ChironeWp3 (3 blocker verificati):**
|
||||
|
||||
- **Nessun teardown dei documenti.** `phase reopen` (`phase_cmd.py:102-127`) fa solo `append_decision`; nessuna I/O su file. Conseguenza: `schema_linking.json`, `ctes/*.sql`, `sql_final.sql` persistono stale dopo il reopen. Bug latente confermato: CTE orfani (non più nel plan riderivato) restano su disco e **bloccano `finalize`** perché itera `glob("*.sql")` richiedendo che ognuno sia testato (`session_cmd.py:192-204`).
|
||||
- **Gli helper NON sono reopen-aware.** `current_phase` (il fold, `phase.py:36-47`) è corretto, ma tutti gli altri helper leggono il ledger intero ignorando il boundary di reopen: `approved_ctes` (`phase.py:125-126`), `_has_decision`/`_has_decision_subject` (`phase.py:138-143`, usati da `advance_problems`), `build_evidence_entries` (`artifacts.py:30-39`), `_compute_promotions` (`memory.py:135`). Mescolano decisioni stale pre-reopen con quelle nuove.
|
||||
- **Manca la granularità step.** "Torna indietro" (`doGoBack`, `nsp-gate.js:295-327`) apre sempre il phase-picker → reopen di fase intera. Non esiste "ripresenta il widget corrente, scarta l'ultima risposta" perché il ledger è append-only senza tombstone/retract.
|
||||
|
||||
**Le tre granularità richieste:**
|
||||
|
||||
(a) **Re-ask current widget (scarta l'ultima risposta, stessa fase).** Il revisore risponde male a una domanda e vuole ridarla. Semantica: l'ultima decisione registrata per il widget corrente viene **ritirata** (tombstone/retract nel ledger), il widget viene ripresentato. Non cambia la fase.
|
||||
(b) **Torna all'inizio dello step precedente del workflow.** Reopen della fase precedente, con teardown di tutti gli artefatti e decisioni da lì in poi.
|
||||
(c) **Torna a uno step specifico.** Reopen di una fase arbitraria precedente, stesso teardown.
|
||||
|
||||
**Architettura del rollback corretto (cosa serve in ThothII):**
|
||||
|
||||
1. **Vista ledger "effective as of pointer".** Un'unica funzione `effective_decisions(session)` che tutti gli helper consultano, invece degli scan ad-hoc. Implementazione: replay del ledger troncando all'ultima `phase_reopened` (o usando un generation counter / high-water-mark). `approved_ctes`, `advance_problems`, `build_evidence_entries`, `_compute_promotions` e chiunque legga il ledger **deve** passare da qui. È la singola fix architetturale più importante.
|
||||
2. **Retract/tombstone per la granularità step.** Per (a) serve poter ritirare l'ultima decisione senza cancellare la riga (audit). Introdurre un decision type `decision_retracted` con `subject` = `decision_seq` della decisione ritirata; la vista "effective" lo onora (la decisione ritirata non conta più, ma resta nell'audit). In alternativa, uno slot per-step mutabile distinto dal log di audit — più invasivo, si valuta.
|
||||
3. **Teardown degli artefatti al reopen.** `phase reopen` deve cancellare gli artefatti prodotti da fasi > target e ricalcolare lo stato derivato. Una funzione `teardown_to_phase(session, target)` che: cancella `schema_linking.json`/`cte_plan.json`/`ctes/*.sql`/`cte_tests.json`/`sql_final.sql`/`evidence.json` a seconda della fase target (la mappa fase→artefatto è nota dal `workflow.yaml`, §5.3 — ogni fase dichiara `artifacts_out`); ricrea quelli della fase target allo stato "vuoto/da produrre"; ricalcola `evidence.json` e l'insieme dei CTE approvati dalla vista effective. Risolve il bug degli orfani.
|
||||
4. **Widget UI per le tre granularità.** Il widget `select` con linkage `option.opens` (§4.2) le copre:
|
||||
- "Rispondi di nuovo a questa domanda" → semantica (a), ritira l'ultima decisione del widget corrente, ripresenta.
|
||||
- "Torna all'inizio della fase precedente" → semantica (b), reopen + teardown.
|
||||
- "Torna a una fase specifica…" → linkage a un widget `select` phase-picker → semantica (c), reopen + teardown.
|
||||
Il frontend deve rendere chiara la distinzione e la conseguenza ("questo cancellerà X e Y").
|
||||
|
||||
**Invariante forte (da enforcement nel gate):** dopo ogni rollback, lo stato di sessione (ledger effective + artefatti su disco + fase derivata) deve essere **coerente** — non deve esistere artefatto stale né decisione stale che conti. Un check `session consistency` (parte di `nsp session check`) lo verifica; se fallisce, il rollback non è completo.
|
||||
|
||||
### 4.9 Minimizzazione del contesto per LLM medio (D16)
|
||||
|
||||
Questa sezione specifica D16. Il requisito: ogni passaggio deve dare al modello **un singolo documento di task con esattamente le informazioni necessarie, derivate dagli step precedenti** — non la conversazione accumulata. Target: modello 35B, contesto <200k.
|
||||
|
||||
**Stato di ChironeWp3 (buona base, ma 3 gap):**
|
||||
|
||||
- **Base giusta:** il design è "context-fresh dagli artefatti via `nsp`" — la skill istruisce comandi per-fase (`nsp search`, `nsp schema render --table`, `nsp memory search`), non replay della chat. Gli artefatti di sessione sono piccoli (KB).
|
||||
- **Gap 1 — niente task document compilato.** Non esiste un generatore che produce "question + schema-linking-deciso + il tuo task per questa fase" in un singolo documento minimale. Il modello deve auto-assemblare il contesto lanciando i comandi giusti.
|
||||
- **Gap 2 — niente enforcement dei bound.** Nulla impedisce al modello di leggere artefatti fatali. **`physical.yaml` = 760KB ≈ 190k token, `report.md` = 344KB** — entrambi fatali per un 35B/<200k. La skill steer-a su `mschema-text --table` (scoped, piccolo) ma è solo una raccomandazione.
|
||||
- **Gap 3 — niente compaction della chat.** Il gate inietta un kickoff one-shot e fa steer, ma **nessun pruning/summarization**. La conversazione cresce senza bound; su 8 fasi un 35B esaurisce la finestra.
|
||||
|
||||
**Architettura del context minimization (cosa serve in ThothII):**
|
||||
|
||||
1. **Generatore di task document per-step.** Un componente `task_doc(session, phase, step)` che legge gli artefatti precedenti e gli input della fase, e emette un **singolo documento compatto** con: la domanda (originale + riscritta), lo schema-linking deciso (solo le tabelle/colonne promosse, non tutto lo schema), i grounding/formula approvati (D14), l'output dei CTE precedenti (riassunto, non intero), e il task specifico della fase/step corrente. Questo documento è **l'input primario del modello per il passo** — non la chat.
|
||||
2. **Enforcement dei bound (deny-list di letture fatali).** Il gate (o il layer RPC) blocca le letture di artefatti integrali che sforano il budget: `physical.yaml`, `report.md` integrale, e in generale qualsiasi file > soglia (es. 50KB) senza scope. Lo schema arriva al modello **solo** come slice scoped (`mschema-text --table <promoted>`) compilato nel task document. Questo è l'invariante che protegge il 35B dal collasso.
|
||||
3. **Compaction della chat (per-fase).** All'inizio di ogni fase, la chat precedente viene **ricompattata**: il modello riparte dal task document della fase + un riassunto minimale delle decisioni chiave delle fasi precedenti (estratto dal ledger effective, §4.8), non dal transcript integrale. Il transcript completo resta accessibile (audit) ma non è nel contesto attivo. Meccanismo: il gate svuota/resetta il contesto attivo del modello all'inizio di ogni fase, consegnando il task document + il brief delle decisioni. (Dettaglio implementativo: dipende dalle capability di Pi di gestire il contesto; da verificare nel piano harness.)
|
||||
|
||||
**Budget indicativo per un 35B / <200k:**
|
||||
- Task document per-step: target <20k token (schema scoped + stato + task).
|
||||
- Brief decisioni fasi precedenti: target <5k token.
|
||||
- Lascia ~175k token per il reasoning del modello sul task — abbondante per un singolo step.
|
||||
|
||||
**Relazione con il rollback (D15):** il task document è generato dalla **vista effective** del ledger (§4.8), quindi post-rollback riflette automaticamente lo stato corretto (decisioni stale escluse). Le due decisioni sono complementari: il rollback produce uno stato coerente, il context minimization lo serve al modello in forma compatta.
|
||||
|
||||
**Impatto sui test (D10):** golden test che verificano (a) il task document di una fase contiene esattamente il slice atteso (non artefatti integrali), (b) una lettura di `physical.yaml` è bloccata, (c) post-rollback il task document esclude le decisioni stale.
|
||||
|
||||
---
|
||||
|
||||
## 5. Modelli dati
|
||||
|
||||
### 5.1 Workspace — `harness/workspaces/<name>.yaml`
|
||||
|
||||
```yaml
|
||||
name: chirone
|
||||
description: "Datawarehouse Policlinico San Donato"
|
||||
|
||||
relational:
|
||||
db_type: postgres # postgres|mariadb|sqlserver|informix|sqlite
|
||||
transport: rest # direct|rest (rest=PostgREST, direct=nativo)
|
||||
# --- per transport: direct ---
|
||||
host: ${CHIRONE_DB_HOST}
|
||||
port: 5432
|
||||
database: datawarehouse
|
||||
schema: datawarehouse
|
||||
user: ${CHIRONE_DB_USER}
|
||||
password: ${CHIRONE_DB_PASSWORD}
|
||||
# --- per transport: rest (PostgREST) ---
|
||||
# rest: { base_url: ${CHIRONE_REST_URL}, api_key: ${CHIRONE_REST_KEY}, ssl_ca: ... }
|
||||
# --- eventuale tunnel ssh (pattern thoth_sqldb2) ---
|
||||
# ssh: { enabled: true, host: ..., username: ..., auth: private_key, key_path: ... }
|
||||
|
||||
vector_db:
|
||||
collection: chirone_docs # tabella pgvector target (schema_records|evidence|memory)
|
||||
dim: 768
|
||||
# --- LOADING diretto (server-only, come ChironeWp3 vector_db): ricostruzione distruttiva ---
|
||||
# local:
|
||||
# host: localhost
|
||||
# port: 5438
|
||||
# database: postgres
|
||||
# schema: vectors
|
||||
# --- LETTURA via REST remota (rpc search_similar, key reader) ---
|
||||
rest:
|
||||
base_url: ${THOTH_VEC_REST_URL} # es. https://host/vector/v1/
|
||||
api_key: ${THOTH_VEC_API_KEY} # header X-API-Key, read-only
|
||||
ssl_ca: ${THOTH_SSL_CA} # opzionale, CA interna
|
||||
# --- SCRITTURA via REST remota (upsert/hash via RPC allowlist, key writer) ---
|
||||
# OPZIONALE: assente o key vuota = scrittura non abilitata (solo lettura).
|
||||
# Abilita nsp memory save-one / vector index-schema su postazione remota.
|
||||
write_rest:
|
||||
base_url: ${THOTH_VEC_REST_URL} # stessa URL del reader
|
||||
api_key: ${THOTH_VEC_WRITE_API_KEY} # key SEPARATA, ruolo vector_writer
|
||||
ssl_ca: ${THOTH_SSL_CA}
|
||||
|
||||
evidence:
|
||||
source_root: ${EVIDENCE_ROOT}
|
||||
evidence_dir: evidence/chirone
|
||||
|
||||
embeddings:
|
||||
provider: ollama # interfaccia embed(texts)→vectors; 1 impl nell'MVP
|
||||
base_url: ${OLLAMA_URL} # http://localhost:11434
|
||||
model: nomic-embed-text-v2-moe
|
||||
dim: 768
|
||||
batch_size: 64
|
||||
|
||||
execution: # fonte di verità read-only (D7 rischio)
|
||||
allow: [cte_test, explain, preview, aggregate, export]
|
||||
max_preview_rows: 10
|
||||
max_export_rows: 10000
|
||||
statement_timeout_ms: 5000
|
||||
# esempio: funzioni che permettono side-effect o escalation privilegi
|
||||
forbidden_functions: [set_config, dblink, dblink_exec, lo_import]
|
||||
```
|
||||
|
||||
Il modulo `workspace.py` carica + valida + espande `${VAR}` dal `.env`. Una sola fonte di verità per `execution.allow`, condivisa tra `nsp` (validazione durante il workflow) e backend (SQL finale read-only).
|
||||
|
||||
### 5.2 Session — `harness/sessions/<id>/`
|
||||
|
||||
Eredita il modello di ChironeWp3:
|
||||
|
||||
```
|
||||
sessions/<id>/ # <id> = YYYY-MM-DD-HHMMSS-<slug>
|
||||
├── session_manifest.yaml # id, created_at, author, status, question, database, schema, schema_version
|
||||
├── question.md # "# Domanda" + "## Assunzioni"
|
||||
├── review_decisions.jsonl # VERITÀ: ledger append-only
|
||||
├── schema_linking.json # candidates[], joins[], excluded[], open_questions[]
|
||||
├── cte_plan.json # [ "pazienti_base", "ricoveri_recenti", ... ]
|
||||
├── ctes/<name>.sql # un file per CTE
|
||||
├── cte_tests.json # CteTestRecord[]
|
||||
├── sql_final.sql # SQL finale approvato
|
||||
├── evidence.json # evidence usate/scartate (a finalize)
|
||||
├── validation_report.md # parsing/read-only/EXPLAIN/preview (a finalize)
|
||||
└── risultati_<ts>.csv # export (su richiesta)
|
||||
```
|
||||
|
||||
**Campi nuovi nel manifest per ThothII** (PRD richiede):
|
||||
- `author`: id utente autenticato (D6)
|
||||
- `summary`: domanda sintetica
|
||||
- `updated_at` + `updated_by`: timestamp e autore ultima modifica
|
||||
- `schema_version`: versione del workflow usato (F2, vedi §5.4)
|
||||
|
||||
**Fase corrente = derivata, non memorizzata.** Chronological fold del ledger (come ChironeWp3 `phase.py`): ogni `phase_approved`/`phase_auto_approved` avanza, `phase_reopened` torna indietro. Il `review_decisions.jsonl` è la verità.
|
||||
|
||||
### 5.3 Workflow — `harness/workflow.yaml` (unica fonte di verità)
|
||||
|
||||
Questa è la principale deviazione architetturale da ChironeWp3, introdotta per abilitare la flessibilità futura del workflow (semplificazione, riordino, fasi opzionali) che il PRD lascia aperta.
|
||||
|
||||
**Problema risolto:** in ChironeWp3 la forma del workflow è codificata in 4 posti indipendenti (`phase.py` con `MAX_PHASE=8`, `PHASE_NAMES`, `SCHEMA_LINKING_PHASE=5`, `DECISION_MIN_PHASE` 16-entry, `advance_problems` ladder `if phase==N`; `nsp-gate.js` con costanti mirrorate — già driftato: il `PHASE_NAMES` JS ha solo 7 entry e manca la fase 8; `SKILL.md` in prose; `finalize` con secondo enforcement point). Cambiare il workflow significa toccare 4 posti in 2 linguaggi.
|
||||
|
||||
**Soluzione:** una sola definizione data-driven.
|
||||
|
||||
```yaml
|
||||
# harness/workflow.yaml
|
||||
schema_version: 1
|
||||
|
||||
phases:
|
||||
- id: F1
|
||||
name: chiarimento
|
||||
advance: kind:phase # meccanismo di chiusura
|
||||
prerequisites: [] # data-driven, no ladder if==N
|
||||
- id: F2
|
||||
name: memoria
|
||||
advance: auto_if_empty # auto-advance se 0 decisioni sostanziose
|
||||
prerequisites: []
|
||||
- id: F3
|
||||
name: riscrittura
|
||||
advance: kind:phase
|
||||
prerequisites:
|
||||
- decision_exists: question_rewritten
|
||||
- id: F4
|
||||
name: schema_linking
|
||||
advance: reviewer_decide
|
||||
prerequisites: []
|
||||
- id: F5
|
||||
name: sintesi
|
||||
advance: kind:phase
|
||||
prerequisites:
|
||||
- file_validates: [schema_linking.json, SchemaLinking]
|
||||
- id: F6
|
||||
name: cte
|
||||
advance: auto_if_empty_or_skipped
|
||||
prerequisites:
|
||||
- any:
|
||||
- decision_subject_exists: [phase_skipped, "phase:6"]
|
||||
- all_ctes_approved: true
|
||||
- id: F7
|
||||
name: sql_finale
|
||||
advance: kind:phase
|
||||
prerequisites:
|
||||
- decision_exists: sql_approved
|
||||
- id: F8
|
||||
name: datamart
|
||||
advance: reviewer_decide
|
||||
prerequisites:
|
||||
- any:
|
||||
- decision_exists: datamart_requested
|
||||
- decision_exists: datamart_declined
|
||||
|
||||
decision_min_phase: auto # DERIVATO dall'ordine delle fasi
|
||||
max_phase: auto # = len(phases)
|
||||
```
|
||||
|
||||
**Come si ottiene la flessibilità:**
|
||||
|
||||
- **`phase.py` legge `workflow.yaml`.** `max_phase = len(phases)` (non più hardcoded). `advance_problems` valuta i `prerequisites` della fase (fine della ladder `if==N`). `decision_min_phase` derivato dalla posizione della fase che emette quel decision type. Nessuna costante duplicata.
|
||||
- **`nsp-gate.js` non mirrora più niente.** Legge i metadati via un nuovo comando `nsp phase meta --json` (restituisce `max_phase`, `PHASE_NAMES`, `SCHEMA_LINKING_PHASE`, advance strategy per fase). Fine del drift JS/Python (il bug F8 scompare).
|
||||
- **`SKILL.md` generato o validato vs `workflow.yaml`.** Le sezioni "## Fase N" possono essere generate da `workflow.yaml`; in alternativa un check assicura che skill e yaml siano allineati.
|
||||
- **`schema_version` nel manifest.** `SessionManifest` porta la versione del workflow usato. La funzione `current_phase` interpreta il ledger secondo la versione, consentendo di evolvere il workflow senza rompere le sessioni esistenti.
|
||||
- **`finalize` legge gli stessi `prerequisites`.** Un solo enforcement point, non due.
|
||||
|
||||
**Le 4 trasformazioni rese fattibili:**
|
||||
- Ridurre 8→5 fasi (fondere): edit `workflow.yaml`, i prerequisites data-driven si adattano.
|
||||
- Riordinare: edit l'ordine in yaml, `decision_min_phase` si ricalcola.
|
||||
- Aggiungere una fase: aggiungi una entry in yaml.
|
||||
- Rendere una fase skippable: aggiungi `prerequisites: any: [decision_subject_exists: [phase_skipped, "phase:N"], ...]`.
|
||||
|
||||
**Cosa si porta da ChironeWp3 come punto di partenza** (da rivalutare in `harness/`, non assunto affidabile per inerzia — vedi §1): ledger append-only, fold cronologico come meccanismo (count-agnostic), `decision_seq` come foreign key, reopen generalizzata (a qualsiasi fase precedente), modello memoria (`REUSABLE_TYPES`, `decision_seq`), i 22 `decision.type`. Ciascuno va verificato e coperto dai golden test (D10).
|
||||
|
||||
### 5.4 Vector DB — modello di accesso a doppia API key (D11)
|
||||
|
||||
Il vector DB (pgvector) è raggiunto via REST con **due endpoint separati, due API key distinte**, per permettere alle postazioni remote di salvare Memory senza poter fare operazioni distruttive. Modello derivato dalla verifica del codice ChironeWp3 (`config.py`, `vectorstore/rest_client.py`, `vectorstore/rest_writer.py`, `cli/_guards.py`, `scripts/create_vector_writer_rpc.sql`).
|
||||
|
||||
**Due ruoli, due config:**
|
||||
|
||||
- **Reader** (`vector_db.rest`, config `RestConfig | None`). Header `X-API-Key: ${THOTH_VEC_API_KEY}`. Allowlist RPC: `search_similar`, `list_tables`. Sola lettura. Sempre necessaria per `nsp search` (RRF).
|
||||
- **Writer** (`vector_db.write_rest`, config `RestConfig | None`, **opzionale**). Header `X-API-Key: ${THOTH_VEC_WRITE_API_KEY}`. Allowlist RPC: `existing_vector_hashes`, `upsert_vector_records`. Upsert + hash sync, **no delete/clear**.
|
||||
|
||||
**Un unico client HTTP, parametrizzato dalla config.** `VectorRestClient(cfg: RestConfig)` è la stessa classe per reader e writer; quale `RestConfig` gli viene passata determina key e allowlist. "Writer realmente configurato" = sezione presente **e** `api_key` non vuota (`has_vector_write_rest` controlla entrambi).
|
||||
|
||||
**Contratto RPC del writer (load-bearing, definito server-side in `scripts/create_vector_writer_rpc.sql`):**
|
||||
|
||||
- `existing_vector_hashes(table_name text, kinds text[]) → table(record_key text, content_hash text)` — restituisce gli hash correnti per il sync differenziale.
|
||||
- `upsert_vector_records(table_name text, rows jsonb) → jsonb` (`{"upserted": N}`) — `ON CONFLICT (record_key) DO UPDATE`, aggiorna `kind/content_hash/metadata/embedding/indexed_at`. **No delete.**
|
||||
- Entrambi `SECURITY DEFINER`, `set search_path = public, vectors, extensions`, `REVOKE` da `public`/`anon`/`authenticated`, `GRANT EXECUTE` solo al ruolo `vector_writer`. La key writer mappa su `vector_writer` → **EXECUTE sulle funzioni, nessun DELETE sulle tabelle raw**.
|
||||
- Tabelle/kinds ammessi: `schema_records` (schema_table, schema_column), `evidence` (evidence), `memory` (memory).
|
||||
|
||||
**Hash dedup client-side.** `content_hash` = SHA-256 del content. Il writer confronta gli hash ricalcolati con `existing_vector_hashes`, embedda solo i record new/changed, li upserta. Idempotente per costruzione.
|
||||
|
||||
**Gating (3 guard functions in `nsp/cli/_guards.py`, punto di partenza da ChironeWp3 da portare e rivalutare in `harness/`):**
|
||||
|
||||
- `require_server_profile(cfg, command)` — operazioni distruttive/server-only (`vector init`, `memory clear`, `memory promote`, `memory update`, `memory delete`): **exit 4** su profilo `workstation` sempre.
|
||||
- `has_vector_write_rest(cfg)` — predicato "writer realmente configurato" (sezione presente **e** key non vuota).
|
||||
- `require_vector_write_allowed(cfg, command)` — operazioni di upsert (`vector index-schema`, `evidence index`, `memory index`, e il nuovo `memory save-one`): permesse su `workstation` **solo se** `has_vector_write_rest`, exit 4 altrimenti. Su `server` sempre permesse.
|
||||
|
||||
**Salvataggio mirato delle Memory — nuovo comando `nsp memory save-one` (D11, deviazione controllata).**
|
||||
|
||||
In ChironeWp3 il salvataggio delle Memory su pgvector avviene solo via `memory index` (resync completo dell'intero registro) o `memory promote` (server-only). Per ThothII si introduce `nsp memory save-one <decision_seq>`:
|
||||
|
||||
- Esegue un **singolo upsert mirato** (un record) del record di memoria associato a quel `decision_seq`, via writer key.
|
||||
- Usa le RPC esistenti `existing_vector_hashes` + `upsert_vector_records` (nessuna nuova RPC server-side).
|
||||
- Hash dedup client-side: embedda solo se il content è cambiato.
|
||||
- Gated da `require_vector_write_allowed` → funziona su postazione remota se la writer key è configurata.
|
||||
- Il workflow (F5 sintesi / F2 memoria) lo chiama quando una Memory viene promossa/accettata per la sessione corrente, invece di scatenare un resync completo.
|
||||
|
||||
**Perché `save-one` invece di `memory index`:** il caso d'uso reale è "salvare la memory appena generata in F5", non "resyncare tutto il registro". Un resync intero è sovradimensionato e rallenta il flusso interattivo. `save-one` è efficiente, idempotente (hash dedup), e abilita il lavoro remoto — che è lo scopo esplicito della doppia key.
|
||||
|
||||
**Profilo `workstation` vs `server` (variabile `THOTH_PROFILE`):**
|
||||
|
||||
- `server` (default): ricostruzione distruttiva completa via `vector_db.local` diretto (init, clear, rebuild). Le guard server-only passano.
|
||||
- `workstation`: blocca init/clear/promote/update/delete (exit 4). Permette upsert (`index-schema`, `evidence index`, `memory index`, `memory save-one`) **solo se** `write_rest` configurato.
|
||||
|
||||
**Variabili d'ambiente (`.env`):**
|
||||
|
||||
- `THOTH_PROFILE` — `server` | `workstation`
|
||||
- `THOTH_VEC_REST_URL` — URL unica per reader e writer
|
||||
- `THOTH_VEC_API_KEY` — key reader (search_similar)
|
||||
- `THOTH_VEC_WRITE_API_KEY` — key writer (upsert), solo se upsert remoto abilitato
|
||||
- `THOTH_VEC_HOST/PORT/USER/PASSWORD` — `vector_db.local` diretto, server-only (in remoto: segnaposto)
|
||||
- `THOTH_SSL_CA` — path CA per HTTPS interno (alimenta `rest.ssl_ca` e `write_rest.ssl_ca`)
|
||||
|
||||
---
|
||||
|
||||
## 6. Frontend / UI
|
||||
|
||||
Le decisioni UI derivano dal PRD, formalizzate durante il brainstorming.
|
||||
|
||||
**Layout — ibrido a 4 zone (Q9-C).** Nav sinistra (funzioni + lista sessioni) | workflow bar orizzontale sopra la chat | chat+input al centro | sidebar destra collassabile con tutti gli artefatti/COT/thinking della sessione.
|
||||
|
||||
**Schema-linking viewer (Q10-A+B).** Mermaid flowchart verticale (default, top-to-bottom) + tabella gerarchica come vista alternativa via toggle. Entrambi vincolati a ≤45 elementi e sviluppo verticale. Commento "perché" inline.
|
||||
|
||||
**CTE / SQL viewer (Q11-A).** Code blocks collassabili (`▾/▸`) con header (nome, n° campi, stato test), SQL formattato + commento per campo. Toggle verticale/orizzontale globale. SQL finale = stesso componente con SELECT espansa e commenti solo su JOIN/WHERE/HAVING/ORDER BY. Evidenziazione sintattica (shiki/highlight.js). Nessun parsing AST richiesto.
|
||||
|
||||
**Pannello risultati (Q12-A).** Contestuale. Numero → grassetto. Lista → AGGrid community con export CSV. Selettore `[10 ▾ / tutti]`. Datamart (dbt/CSV/Excel) come azione separata con conferma pseudoanonimizzazione.
|
||||
|
||||
**Testi lunghi e markdown.** Box con scorrimento orizzontale e verticale. Markdown "mermaid enhanced": formattazione + rendering degli schemi mermaid inclusi.
|
||||
|
||||
---
|
||||
|
||||
## 7. Strategia di implementazione (D9)
|
||||
|
||||
Vertical slice per fase. Ordine harness → backend → frontend, con loop end-to-end precoci. Schema indicativo dei cicli:
|
||||
|
||||
- **Ciclo 0 (harness isolato):** `nsp` risponde a `nsp search/phase/session` in JSON, più il nuovo `nsp phase meta --json` (F2) che il gate userà per non mirrorare le costanti. Testato con script che mandano JSON a mano + fake-Pi con golden test (D10).
|
||||
- **Ciclo 1 (harness + backend minimale):** il BE fa spawn di Pi, forwarda 1 widget-descriptor (F1: disambiguazione). Test end-to-end via curl.
|
||||
- **Ciclo 2 (FE minimale):** aggiungi il frontend minimo (solo F1) per chiudere il loop visivamente.
|
||||
- **Cicli 3+:** estendi fase per fase (F2 memory, F3 rewrite, F4 schema-link, …) su tutti e tre i layer.
|
||||
|
||||
---
|
||||
|
||||
## 8. Flessibilità e parametricità — quadro
|
||||
|
||||
Applicato un filtro secco: flessibilità inclusa solo dove (a) il PRD la chiede, oppure (b) risolve una tensione architetturale già emersa, e il costo è basso.
|
||||
|
||||
**Accettate (6):**
|
||||
|
||||
- **F1 — Widget descriptor con `kind` aperto + fallback.** Costo basso (un campo + renderer fallback). Risolve "future modalità di interazione" (richiesta esplicita). Vedi §4.
|
||||
- **F2 — `workflow.yaml` come unica fonte di verità.** Risolve il drift reale già verificato (JS manca F8). Abilita semplificazione futura. Vedi §5.3.
|
||||
- **F3 — Workspace YAML parametrico su `db_type` + `transport`.** È il requisito del PRD (postgres/sqlserver/mariadb/informix + REST/SSH/diretto). Vedi §5.1.
|
||||
- **F4 — Auth middleware pluggabile `none`/`mock`/`oidc`.** PRD chiede esplicitamente Authentik+Entra ID+no-auth. Una sola codepath OIDC. Vedi D6.
|
||||
- **F5 — `embeddings.provider` nel workspace.** Il layer embeddings è dietro un'interfaccia semplice (`embed(texts)→vectors`, 1 implementazione). Permette swap provider senza toccare RRF.
|
||||
- **F6 — Modello a doppia API key per il vector DB.** Il PRD richiede lavoro remoto (postazione fuori server) con salvataggio Memory controllato. Due key (reader/writer) con allowlist RPC separate è il modo pulito per abilitarlo senza esporre delete/clear. Vedi §5.4, D11.
|
||||
|
||||
**Rifiutate (8) — over-engineering:**
|
||||
|
||||
- **R1 — Registry di adapter DB pluggabile nel backend Node.** Il backend (D7) tocca i DB solo per SQL finale read-only; serve un driver per il workspace corrente, non un registry eterogeneo. La complessità dei 5 DB vive in `nsp` Python (D8). Duplicare thoth_sqldb2 nel backend è senza valore.
|
||||
- **R2 — Sistema di plugin per i widget del frontend.** I 6 widget + fallback (F1) coprono il caso. Un framework di plugin è speculativo: nessuna evidenza di bisogno. Un settimo widget si aggiunge come componente React.
|
||||
- **R3 — Repository pattern / astrazione sulla persistenza session.** Le sessioni sono file su FS (D5), accessi via `nsp`. Avvolgere il FS in `SessionRepository` per swap futuro a DB è YAGNI. `nsp session/store.py` è già il confine.
|
||||
- **R4 — Event sourcing / message bus interno al backend.** Il backend fa solo da traduttore. Non è un sistema event-driven con molti produttori/consumatori. Una message bus aggiunge complessità senza un secondo consumatore.
|
||||
- **R5 — Configurabilità dei widget e del layout via configurazione.** Il PRD descrive una UI specifica. Rendere configurabile il layout = costruirla due volte. YAGNI.
|
||||
- **R6 — Multi-tenancy con workflow diversi per tenant.** L'MVP è mono-utente (D6). Per-tenant workflow è speculativo. Se servirà, "workflow.yaml per-workspace" è un'estensione naturale di F2.
|
||||
- **R7 — Livello di astrazione sul transport FE↔BE (SSE vs WebSocket vs polling).** SSE basta per lo streaming unidirezionale. Astrarrlo per swap futuro è YAGNI.
|
||||
- **R8 — SQL builder driver-agnostic nel backend.** Il backend esegue SQL già generato da `nsp`, non lo costruisce. Un query builder è inutile.
|
||||
|
||||
---
|
||||
|
||||
## 9. Fuori scope (MVP)
|
||||
|
||||
- **Web app centrale multi-utente (modello A, D12):** l'MVP è modello B (postazione remota localhost). L'evoluzione ad A riposiziona backend+harness su server centrale + attiva OIDC (D6); i contratti non cambiano.
|
||||
- Multi-utente reale con concorrenza (D6 prepara il terreno ma l'MVP è mono-operatore in localhost).
|
||||
- CRUD workspace via API (D3: i workspace sono YAML; la scrittura è manuale, il backend li espone in lettura).
|
||||
- Plugin system per DB adapter nel backend (R1), plugin widget FE (R2), repository pattern (R3), event bus (R4), configurabilità layout (R5), multi-tenancy (R6).
|
||||
- Job runner asincrono per export/preview (Q12-A scelta contestuale, sincrono).
|
||||
- Form builder UI per widget futuri (R2).
|
||||
|
||||
---
|
||||
|
||||
## 10. Rischi aperti
|
||||
|
||||
**Rischio D7 — due codepath SQL read-only.** `nsp` (Python) e backend (Node) eseguono entrambi SQL sul workspace. Devono rimanere allineate sull'enforcement read-only. Mitigazione: un'unica fonte di verità (`execution.allow` nel workspace YAML, condivisa). Punto di attenzione nello sviluppo.
|
||||
|
||||
**Rischio fake-Pi — fedeltà del protocollo.** Il fake-Pi (D10) deve riprodurre fedelmente il framing del protocollo RPC di Pi (LF-only, niente `readline`; split su `\n`). Se devia, i golden test non catturano regressioni reali. Mitigazione: basare il fake-Pi sul reference `RpcClient`/`LineSplitter` di ChironeWp3 (`docs/superpowers/plans/2026-06-14-psdwp3-pi-web-console-bridge.md`).
|
||||
|
||||
**Rischio workflow.yaml — curva di adozione.** Introdurre `workflow.yaml` come unica fonte di verità richiede di riscrivere `phase.py` (da ladder `if==N` a evaluation data-driven) e aggiungere `nsp phase meta --json`. È lavoro in più rispetto a "copia ChironeWp3 così com'è", ma è il prezzo della flessibilità futura richiesta. Da bilanciare con D9 (vertical slice): il `workflow.yaml` può entrare gradualmente nei cicli.
|
||||
|
||||
---
|
||||
|
||||
## 11. Nota sulla struttura dei piani di implementazione
|
||||
|
||||
Questo documento è la **vista d'insieme** dell'architettura ThothII. Copre i tre progetti (harness, backend, frontend) e i loro contratti. La fase di `writing-plans` produrrà **tre piani separati**, uno per progetto, nell'ordine della strategia D9 (prima harness, poi backend, poi frontend). Ogni piano è autonomo e referenzia questo spec per i contratti condivisi (widget-descriptor di §4, modelli dati di §5). I contratti tra i layer sono definiti qui una volta per tutte, così ogni piano può essere implementato e testato in modo indipendente contro il contratto — esattamente come richiesto dal PRD ("tre diversi progetti autonomi").
|
||||
@@ -0,0 +1,113 @@
|
||||
# ThothII
|
||||
|
||||
Obiettivo del progetto: costruzione di un sistema che permetta, a partire da una richiesta fatta in linguaggio naturale, di generare un SQL eseguibile su un certo database
|
||||
|
||||
ThothII deve essere scritto in Python e Typescript/Javascript ed appoggiarsi al coding harness Pi (http://pi.dev) per l'esecuzione dei task che devono essere delegati a un AI model.
|
||||
|
||||
Il codice va organizzato in tre layer. ognuno dei quali va sviluppato all'interno di una sua cartella:
|
||||
|
||||
1. un frontend in React, che usa NextJs+ShadCn+AGGrid + qualunque altra libreria di frontend adatta allo scopo;
|
||||
2. un backend scritto in qualunque modo, che si interfaccia con il coding harness Pi per eseguire i task che devono essere delegati a un AI model. Può essere tranquillamente un'applicazione NodeJs+Fastify
|
||||
3. un harness, cioè un insieme di Typescript/Javascript + Skills in markdown che costituiscano l'harness di Pi usato in modalità rpc. E' già stato scritto un harness, disponibile in ./ChironeWp3, che funziona, ma va riscritto tenendo conto che ThothII non prevede la possibilità di interagire direttamente con il coding harness Pi. Il codice di ChironeWp3 va riscritto tenendo conto che Pi deve rispondere solo im json, in modo che le sue risposte possano essere facilmente parsate dal backend ed esposte nel frontend con i giusti widget. Ciò è una importante variazione. Inoltre
|
||||
|
||||
L'architettura può essere oggetto di discussione durante la fase di brainstorming e design.
|
||||
|
||||
## Punti chiave di discussione
|
||||
|
||||
Per facilitare la discussione, riporto in ordine sparso idee e considerazioni varie, tutte potenziale oggetto di arricchimento durante il brainstorming.
|
||||
|
||||
L'applicazione deve avere al centro il workflow di generazione del SQL a partire da una domanda espressa in linguggio naturale, ma deve anche fornire un frontend per l'esecuzione di attività normalmente fatte tramite CLI su Pi come la scelta del model, del tema, del linguaggio, la selezione, il richiamo e la navigazione di una session, ecc. La lista dei comandi di Pi e della lista del setup del frontend React da implementare sarà oggetto di discussione durante il brainstorming ed il design.
|
||||
|
||||
L'aderenza al workflow delineato in ./ChironeWp3 deve essere stretta in quanto funzionante. Potrà essere oggetto di perfezionamento in versioni future, ma il MVP deeve aderire a quanto sviluppato, a parte eventuali bug fix o evidenti miglioramenti applicabili subito.
|
||||
|
||||
L'applicazione, come già fa quella attualmente sviluppata, può contare su tre risorse disponibili collegandosi al server di produzione:
|
||||
|
||||
- un Supabase contenente il datawarehouse per cui si vuole generare il SQL
|
||||
- un pgvector, contenuto anch'esso nel Supabase, che contiene gli embeddings dei documenti che descrivono il datawarehouse
|
||||
- un LLM (qwen 3.6 - 35B) utilizzabile da Pi che gira sulle GPU del server di produzione, ed è quindi gratuito
|
||||
|
||||
all'interno del progetto ./ChironeWp3 vi sono già tutti gli elementi necessari per gestire la connessione col datawarehouse del policlinicosandonato, ma ThothII deve potersi interfacciare con qualunque database e con un pgvector locale nel caso non sia disponibile un pgvector remoto. Per cui deve essere previsto un insime di configurazioni destinate a implementare il concetto di workspace composto da db relazionale + pgvector (locale o remoto) su cui operare prevedendo diverse modalità di accesso (REST, tunnel ssh, accesso diretto) e diverse tipologie di db relazionale (posthres, sqlserver, mariadb ed informix innanzitutto)
|
||||
|
||||
Per quanto riguarda il collegamento ad un database qualunque trovi in ./Thoth/thoth_sqldb2 del codice a cui potersi ispirarsi per l'implementazione di un modulo di connessione a database generico.
|
||||
|
||||
## il workflow
|
||||
|
||||
1. disambiguazione della richiesta fatta
|
||||
2. recupero delle memory per loro utilizzo nella creazione dello schema-linking
|
||||
3. riscrittura ed approvazione della domanda riscritta
|
||||
4. creazione e discussione dello schema-linking
|
||||
5. sintesi dello schema-linking determinato
|
||||
6. costruzione e discussione dei CTE
|
||||
7. generazione e discussione del SQL finale
|
||||
8. visualizzazione, tramite AGGrid, del risultato ed eventuale generazione del dbt in grado di essere eseguito in un flusso ETL
|
||||
|
||||
Questo workflow è già stato implementato in ./ChironeWp3, ma ThothII deve avere una gestne basata su una UI gestita in REACT appositamente per avere un controllo migliore del workflow e dei documenti intermedi che vengono prodotti.
|
||||
|
||||
Prima di tutto ci deve essere, in ThothII, una pagina in cui si vede il workflow, come una specie di lista numerata, com visualizzazione deegli step terminati e possibilità di reset del processo al punto richiamato. Ogni step del workflow deve produrre un documento che sta alla base dello step successivo, in modo da minimizzare il contesto necessario ad ogni fase.
|
||||
|
||||
## L'interfaccia utente
|
||||
|
||||
l'interfaccia deve mostrare i messaggi che arrivano da PI sia come messaggi che spiegano, sia come richieste di scelta tra diverse opzioni o richieste di informazioni tramite chat. Deve però mostrare solo i messaggi principali, mentre i COT e l'eventuale thinking, se presente, devono essere visibili in finestre dedicate, nel sidebar di destra, a richiesta.
|
||||
|
||||
La deve essere divisa in quattro parti, come fa Codex di OpenAI. Un sidebar di sinistra dove avere link verso determinate funzioni e una lista gestibile di sessioni. Una parte centrale divisa in due: la parte bassa come area di input, ed una parte alta dove vengono mostrati i messaggi da parte del modello e dove vengono proposte le possibili risposte alle domande poste dal modello. Una sidebar di destra dove vengono mostratigli gli artefatti come messaggi, schema-linking, COT ed SQL e dove è possibile visualizzare le COT ed il thinking, se presenti.
|
||||
|
||||
Come si può vedere, in buona parte delle volte che il model in ChironeWp3 interagisce con l'utente lo fa in tre modi:
|
||||
|
||||
1. lo informa di quanto sta per fare (le domande per togliere ambiquità alla richresta, o gli elementi dello schema-linking che sta per proporgli
|
||||
2. gli fa la domanda vera e propria, proponendo di selezionare una o più risposte, oppure di inserire un testo libero, o di tornare indietro o di interrompere il processo.
|
||||
3. gli fa vedere il risultato di un blocco di domande-risposte (intero messaggio disambiguato, intero schema-linking, intero SQL generato, ecc.)
|
||||
|
||||
Per ogni tipo di interazione occorre prevedere una specifica forma di widget, ispirata a come fa codex, oppure ZCode o la versione desktop di Claude Code. E ogni output di tipo 3 deve essere correttamente gestito con la parte destra della UI, che deve comparire solo a richiesta, e deve essere possibile salvare ogni output in una pagina separata, con un link che lo riporti alla pagina principale.
|
||||
|
||||
In particolare:
|
||||
|
||||
- i testi lunghi devono essere inseriti in box con scorrimento orizzontale e verticale
|
||||
- i markdown devono essere mostrati in modo "mermaid enanced", nel senso che devono avere la formattazione e mostrare eventuali schemi mermaid inclusi nel testo
|
||||
- gli sql devono essere formattati come codice ed essere presentati in modo colorato, con le liste dei campi delle select espansi in modo orizzontale e con un widget che mi permetta di espandere o collassare le varie parti dello statement SQL
|
||||
|
||||
Ovviamente l'interfaccia utente deve permettere la selezione del workspace su cui si vuole operare, che a sua volta permette la determinazione del DB, del VectorDb e della collection associata, delle evidence associate.
|
||||
|
||||
## La visualizzaione dello schema-link
|
||||
|
||||
Lo schema Link richiede una presentazione particolarmente accurata infatti si tratta di presentare un insieme di tabelle correlate tra di loro i campi di queste tabelle selezionati per essere fonte dei dati e il collegamento concettuale che ha portato a scegliere quei campi e quelle tabelle per estrarre le informazioni richieste. Di conseguenza è necessario prevedere una attenta impostazione nella presentazione di queste informazioni per aiutare l'utente a determinare se si tratta di impostazioni corrette o se bisogna applicare delle modifiche per ottenere il migliore dei risultati importante, quindi sarà l'impostazione grafica che dovrà essere data per comunicare queste informazioni includi tra le possibilità l'uso di Mermaid per rappresentare schemi concettuali, ma mantieni sempre l'obiettivo di contenere ad un massimo di 45 elementi da includere nello schema e di sviluppare lo schema in verticale e non in orizzontale per permetterne una maggiore leggibilità
|
||||
|
||||
## l'visualizzazione dei CTE
|
||||
|
||||
I CTE devono essere presentati in modo che sia possibile espandere e collassare le varie parti del CTE, e che sia possibile visualizzare il CTE in modo orizzontale o verticale.
|
||||
|
||||
Inoltre per ogni campo incluso nel CTE deve esserci un commento che indica il contenuto atteso nel campo e le motivazioni per cui è stato selezionato
|
||||
|
||||
## La visualizzazione del SQL Finale
|
||||
Il seguente finale deve essere presentato in modo che sia leggibile facilmente da parte dell'utente. Quindi tutti i campi delle Select devono essere sviluppati in orizzontale senza preoccuparsi di commentarne il contenuto perché questo è già avvenuto a livello di CTE. Devono essere invece commentati le JOIN le WHERE le HAVING, le ORDER BY e tutti gli altri elementi che sono stati aggiunti a livello di assemblaggio finale
|
||||
|
||||
|
||||
## la gestione delle sessioni
|
||||
|
||||
Le sessioni devono essere:
|
||||
|
||||
- listate e gestite nella sidebar di sinistra
|
||||
- richiamabili con recupero delll'interezza degli artefatti della sessione richiamata e la possibilità di ripartire da un punto del workflow, modificare le risposte ad una domanda e rifare le fasi terminali del processo
|
||||
|
||||
Ogni sessione deve essere evidenziata con un codice autogenerato, l'id github dell'autore, una summary della domanda, la domanda per esteso, u timestamp della data e dell'autore della creazione della sessione, un timestamp con data ed autore dell'ultima modifica.
|
||||
|
||||
## La presentazione dell'esecuzione del SQL prodotto
|
||||
|
||||
L'esecuzione del SQL può produrre un numero, o una lista di elementi presentabili. Nel primo caso il numero deve essere presentato in grassetto, mentre nel secondo caso deve essere usata la libreria AGGrid in versione community per presentare la lista di elementi, con attiva l'opzione di esportazione in csv. Deve essere data la possibilità all'utente di scegliere tra presentare l'intera lista o un estratto di 10 record.
|
||||
|
||||
## La generazione dei Datamart
|
||||
|
||||
Il passo finale previsto dal Workflow deve essere:
|
||||
|
||||
- la generazione di un artefatto utile per essere inserito in un processo ETL. Nel MVP sarebbe il dbt da inserire nel processo ETL implementato in Policlinico San Donato.
|
||||
- la generazione di altro tipo di artefatto secondo indicazioni indicate nei parametri di workspace;
|
||||
- una lista in formato CSV dei dati estratti, sia con nomi, cognomi ed ID pseudoanonimizzati, sia con anagrafica in chiaro
|
||||
- lo stesso tipo di lista ma in formato Excel
|
||||
|
||||
|
||||
## Gli artifacts e le altre impostazioni di ChironeWp3
|
||||
|
||||
Il processo previsto da ThothII si basa, tra le altre cose, sulla presenza di artifacts che comprendono delle Evidence. Prevedere una gestione locale delle evidence, con memorizzazione degi chunk creati e di cui si è fatto l'embedding nel database vettoriale associato al workspace. Ovviamente le evidence devono esssere distinte per workspace.
|
||||
|
||||
## L'autenticazione
|
||||
|
||||
L'applicazione deve prevedere la possibilità di collegarsi via http ad un Identity Manager. Nel MVP deve essere impostata l'autenticazione via Athentik, il quale a sua volta si interfaccia con il sistema di autenticazione del Policlinico San Donato basato su LDAP. Però deve essere anche prevista la possibilità di autenticarsi con un Entra ID. Per cui il sistema deve prevedere la possibilità di collegarsi a più Identity Manager, sostanzialmente tutti OIDC, ma diversi tra loro. Deve però poter operare anche senza autenticazione, sia per facilitare i test e lo sviluppo, sia come condizione potenziale di configurazione anche a sistema sviluppato e ready-for-production
|
||||
Reference in New Issue
Block a user