La prima stesura usava una struttura 'ideale' (relational/vector_db.collection/ embeddings.provider) che non combaciava col modello Config portato da ChironeWp3. Allineato alla struttura reale (database/rest/vector_rest/vector_write_rest/ vector_db top-level). Aggiunta nota di allineamento + modello delle key D11.
729 lines
72 KiB
Markdown
729 lines
72 KiB
Markdown
# 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`
|
|
|
|
> **Nota di allineamento (post-brainstorming):** la prima stesura di questa sezione usava una struttura "ideale" (`relational:`, `vector_db.collection`, `embeddings.provider`) che non corrispondeva al modello `Config` reale portato da ChironeWp3. Durante il Task A2 del piano harness si è scelto (decisione B) di **allineare spec e codice alla struttura reale di `Config`**, perché è uno dei pochi pezzi riusati quasi tal quali e ristrutturarlo avrebbe propagato il cambio a tutti i moduli che lo leggono. La struttura canonica verificata è `harness/workspaces/chirone.example.yaml`; il blocco YAML sotto è un estratto che riflette `nsp/config.py` fedelmente (sezioni `database`/`rest`/`vector_db`/`vector_rest`/`vector_write_rest`/`embeddings`/`evidence`/`execution`, tutte top-level; nessun `relational:` o `vector_db.collection`). Per l'esempio completo fare riferimento al file `chirone.example.yaml`.
|
|
|
|
```yaml
|
|
# Estratto — struttura reale del Config (vedi chirone.example.yaml per il completo).
|
|
database: # DWH relazionale
|
|
host: ${THOTH_DB_HOST}
|
|
transport: rest # direct | rest
|
|
rest: # richiesto se transport=rest
|
|
base_url: ${THOTH_DWH_REST_URL}
|
|
api_key: ${THOTH_DWH_API_KEY} # ruolo dwh_reader
|
|
vector_rest: # LETTURA pgvector (rpc search_similar), key reader
|
|
base_url: ${THOTH_VEC_REST_URL}
|
|
api_key: ${THOTH_VEC_API_KEY}
|
|
vector_write_rest: # SCRITTURA pgvector (upsert/hash), key writer SEPARATA, opzionale
|
|
base_url: ${THOTH_VEC_REST_URL}
|
|
api_key: ${THOTH_VEC_WRITE_API_KEY}
|
|
vector_db: # LOADING diretto pgvector, server-only
|
|
host: ${THOTH_VEC_HOST}
|
|
embeddings:
|
|
base_url: ${THOTH_OLLAMA_URL}
|
|
evidence:
|
|
source_root: ${THOTH_DOCS_ROOT}
|
|
execution: # fonte verità read-only (D7)
|
|
allow: [cte_test, explain, preview, aggregate, export]
|
|
```
|
|
|
|
Il modulo `workspace.py` (confine D3) carica + valida + espande `${VAR}` dal `.env`, delegando a `load_config` (portato da ChironeWp3). Una sola fonte di verità per `execution.allow`, condivisa tra `nsp` (validazione durante il workflow) e backend (SQL finale read-only).
|
|
|
|
**Modello delle key (D11, vedi §5.4):** il `Config` ha tre sezioni `RestConfig` indipendenti con key distinte — `rest.api_key` (DWH reader), `vector_rest.api_key` (pgvector reader), `vector_write_rest.api_key` (pgvector writer). Nel deployment Chirone reale la key del DWH reader e quella del pgvector reader **condividono lo stesso valore** (key unica validata da Nginx), ma restano campi separati nella config per chiarezza e flessibilità.
|
|
|
|
### 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").
|