diff --git a/docs/superpowers/specs/2026-06-27-backend-design.md b/docs/superpowers/specs/2026-06-27-backend-design.md new file mode 100644 index 00000000..4a1dd04a --- /dev/null +++ b/docs/superpowers/specs/2026-06-27-backend-design.md @@ -0,0 +1,189 @@ +# ThothII — Design del backend + +**Data:** 2026-06-27 +**Stato:** Draft, in attesa di review +**Fonti:** `docs/superpowers/specs/2026-06-25-thothii-architecture-design.md` (architettura d'insieme), `docs/l2-run-report-2026-06-27.md` (primo run RPC end-to-end), codice `harness/` (CLI `tht` + gate `tht-gate.js`), `pi --help` di `@earendil-works/pi-coding-agent`. + +--- + +## 1. Obiettivo e contesto + +L'harness di ThothII è completo e validato (L0/L1 deterministici + un run L2 reale con GLM 5.2). Il report L2 del 2026-06-27 ha messo a fuoco il gap che questo progetto colma: + +> *"La modalità RPC emette i widget come JSONL su stdio per un client esterno — che non esiste ancora in ThothII."* + +**Il backend È quel client esterno mancante.** È il componente che rende l'harness usabile end-to-end: avvia Pi in modalità RPC, fa da ponte tra il protocollo JSONL del gate e il frontend (SSE + REST), e delega a `tht` l'esecuzione controllata del SQL finale. + +Questo documento è autonomo ma referenzia l'architettura d'insieme per i contratti condivisi: il **widget-descriptor** (spec architetturale §4) e i **modelli dati** di sessione/workspace (spec §5). Non li ridefinisce. + +**Posizione nel piano generale:** secondo dei tre progetti (harness → **backend** → frontend), come previsto da D1/D9 e §11 dell'architettura. Un solo piano di implementazione per il backend, costruito a slice incrementali con un primo loop end-to-end F1 il prima possibile. + +--- + +## 2. Decisioni del backend (locked) + +Decise durante il brainstorming del 2026-06-27. Ogni voce riporta la scelta e il perché. + +**BE-1 — Un processo Pi per sessione attiva.** +Il backend fa spawn di un processo `pi --mode rpc` (cwd = `harness/`) per ogni sessione **attiva** (aperta dall'operatore). Riprendere una sessione = respawn puntato sull'id esistente; il ledger su disco (`review_decisions.jsonl`) è la verità, quindi lo stato sopravvive al teardown del processo. +Perché: isolamento totale tra sessioni, coerente con il modello a sessioni multiple del FE. L'MVP è mono-operatore localhost (D12-B), quindi il numero di processi concorrenti è basso e gestibile con un cap di sicurezza (vedi §5). + +**BE-2 — Esecuzione del SQL finale delegata a `tht` (codepath unico).** +Il backend **non si collega mai direttamente al DB**. Per alimentare AGGrid chiama `tht sql preview` (con paginazione), per l'export chiama `tht sql export`. Riusa enforcement read-only, `execution.allow`, transport (`direct`/`rest`) e gestione dialetto già esistenti e testati in `tht`. +Perché: **elimina il rischio architetturale D7** (due codepath SQL da tenere allineate sul read-only). Lo spec architetturale lo scelse come rischio aperto; qui lo chiudiamo. Costo misurato: ~190 ms di overhead per chiamata (cold-start dell'interprete Python), perché il round-trip al DWH (~40–80 ms via REST) è identico nei due approcci. Irrilevante per l'export, tollerabile per la paginazione di uno strumento di review, abbattibile post-MVP con un worker `tht` persistente se mai servisse. +Questa è una **deviazione esplicita da D7** ("il backend si collega al DB del workspace"): il backend resta un orchestratore/traduttore puro, senza client DB proprio. + +**BE-3 — Resilienza: ricostruzione da disco + re-emit del widget pendente.** +Al (ri)caricamento il FE ricostruisce lo stato della sessione via REST (ledger + artefatti su disco = verità). Alla (ri)sottoscrizione SSE il backend **ri-emette solo la `ui_request` attualmente pendente** (quella che il modello sta aspettando), che già traccia per la correlazione. Nessun buffer/event-store. I `text_delta` persi durante un disconnect non si recuperano (accettabile: conta l'artefatto/widget finale). Un restart del backend termina i processi Pi figli; al resume di una sessione, Pi viene respawnato. +Perché: il modello più semplice che copre i casi reali del mono-operatore (chiudo/riapro tab, riavvio backend, crash Pi) senza introdurre un event log con retention da gestire (over-engineering per l'MVP). + +**BE-4 — Test: fake-Pi condiviso + unit test TS.** +Si costruisce **un** fake-Pi: uno script che parla il protocollo RPC di Pi su stdio, scriptato per emettere sequenze fisse di `extension_ui_request`. Serve sia i test d'integrazione del backend (spawn + stdio + framing LF-only + correlazione **reali**) sia i golden test dell'harness (colma il gap D10 segnalato dall'L2). In più, unit test TS sulla logica pura di traduzione/correlazione. Il livello L2 con Pi reale resta separato e informativo (non-deterministico, richiede VPN/credenziali). +Perché: l'unica strategia che testa in modo deterministico la parte più fragile (framing stdio + correlazione) senza LLM né rete in CI. + +**BE-5 — Il backend pre-crea la sessione e possiede l'id.** +Su `POST /sessions` il backend esegue lui `tht session new` (conosce subito id e directory), poi fa spawn di Pi **iniettando l'id già creato nel kickoff** (`/nuova-domanda`). Il modello usa la sessione, non la crea. +Perché: l'alternativa (il modello crea la sessione, il backend ne "scopre" l'id osservando la dir o parsando l'output) è fragile e soggetta a race. Backend padrone dell'id e del ciclo di vita. Richiede una piccola modifica al kickoff/gate dell'harness (id fornito invece che creato dal modello) — vedi §7. + +**BE-6 — model/thinking/provider per-sessione, persistiti, riapplicati al resume.** +`POST /sessions` accetta `{provider?, model?, thinking?}` opzionali; in mancanza usa i default da config del backend. I tre valori si **persistono nel manifest di sessione** e si **riapplicano al respawn** in fase di resume. Niente cambio a sessione in corso nell'MVP. +Perché: copre il caso d'uso principale ("scelgo il modello giusto per questa domanda") in modo deterministico. Pi supporta nativamente `--provider/--model/--thinking` componibili con `--mode rpc` (verificato). + +**BE-7 — Settings Pi esposti nell'MVP.** +Esposti al FE (per-sessione, hanno flag CLI): `--name` (nome visualizzato sessione). Globali via config operatore (`.pi/settings.json`): `temperature` (bassa di default per determinismo NL→SQL), `maxTokens` (cap output, legato al budget contesto del 35B, D16). Endpoint `GET /models` (via `pi --list-models`) per la tendina FE. **Correttezza dello spawn (non opzionali):** `trust`/`--approve` per fidare i file project-local (gate extension + skill) ed evitare un prompt di trust che appenderebbe il loop RPC; `quietStartup: true` per non sporcare il flusso JSONL letto dal RpcClient; `systemPrompt` **mai** sovrascritto (il comportamento è guidato da kickoff + skill; un override romperebbe il gate). +Perché: `temperature`, `maxTokens`, `quietStartup`, `trust`, `systemPrompt`, `tools` **non hanno flag CLI** in Pi → vivono in `.pi/settings.json` (globali al progetto), non sono per-sessione. Limite noto: `temperature`/`maxTokens` per-sessione non sono possibili oggi senza un flag che Pi non espone. + +--- + +## 3. Architettura e flusso dei dati + +Il backend è un **orchestratore/traduttore senza stato persistente proprio**: la verità sta su disco (ledger + artefatti dell'harness). In RAM tiene, per ogni sessione attiva, solo l'handle del processo Pi e la `ui_request` pendente. Stack: **Node.js + Fastify + TypeScript**. Deployment MVP: localhost, mono-operatore (D12-B). + +``` +┌─ FRONTEND (frontend/) ──────────────────────────────────────────────┐ +│ React/Next/ShadCn/AGGrid · consuma SOLO la REST+SSE del backend │ +└──────────────────────────────▲──────────────────────────────────────┘ + │ HTTP/SSE (JSON, localhost) +┌─ BACKEND (backend/) ──────────┴──────────────────────────────────────┐ +│ Node + Fastify + TS │ +│ • PiProcessManager: un `pi --mode rpc` per sessione attiva (BE-1) │ +│ • RpcClient (per Pi): LineSplitter LF-only + dispatch per id │ +│ • SessionBridge: extension_ui_request ↔ ui_request, │ +│ ui_response ↔ extension_ui_response │ +│ • SSE hub: stream per sessione, re-emit del widget pendente (BE-3) │ +│ • Auth middleware: none(MVP) | mock | oidc (D6) │ +│ • Delega SQL: tht sql preview/export (BE-2) — NESSUN client DB │ +│ • REST: workspaces, sessions, artifacts, models │ +└────────────────────────────────┬──────────────┬─────────────────────┘ + JSONL (LF-only)│ │ subprocess `tht … --json` + ▼ ▼ + ┌─ pi --mode rpc ─┐ ┌─ tht (CLI Python) ─┐ + │ gate tht-gate.js│ │ session/sql/phase │ + │ emette widget │ │ … --json │ + └─────────────────┘ └────────────────────┘ + │ │ + └── entrambi leggono ──┘ + harness/sessions// (verità) +``` + +### Flusso di una nuova domanda (BE-5) + +1. FE → `POST /sessions {workspace, question, provider?, model?, thinking?, name?}`. +2. Backend esegue `tht session new` → ottiene `` e la directory di sessione; scrive provider/model/thinking/name nel manifest (BE-6/BE-7). +3. Backend fa spawn di `pi --mode rpc` (cwd=harness, `--approve`, `--provider/--model/--thinking/--name`, `--session-dir`/`--session-id` per agganciare la sessione) e inietta l'id nel kickoff `/nuova-domanda`. +4. Il modello carica la skill `tht-sessione` e avvia F1; il gate emette il primo widget come `extension_ui_request` (JSONL su stdout). +5. RpcClient lo riceve → SessionBridge lo traduce in `ui_request` → SSE hub lo manda al FE; la `ui_request` resta tracciata come "pendente". +6. FE renderizza il widget, l'utente risponde → `POST /sessions/:id/response {ui_response}`. +7. Backend traduce in `extension_ui_response` e lo scrive su stdin di Pi; il gate prosegue, registra la decisione via `tht`, la fase deriva. + +### Flusso del SQL finale (BE-2) + +A workflow concluso (sql_final.sql approvato), il FE richiede i risultati: +- `POST /sessions/:id/sql/preview?limit=&offset=` → backend lancia `tht sql preview --json` (con offset) → righe per AGGrid. +- `POST /sessions/:id/sql/export` → backend lancia `tht sql export` → file CSV in download. + +--- + +## 4. API REST + SSE (contratto FE↔BE) + +Tutti i payload `ui_request`/`ui_response`/`info`/`system_event` seguono il widget-descriptor dell'architettura §4 (non ridefinito qui). Il backend è trasporto puro per quei messaggi. + +| Metodo | Path | Scopo | +|---|---|---| +| `GET` | `/workspaces` | Lista workspace (lettura da `harness/workspaces/*.yaml`, read-only, no CRUD — D3) | +| `GET` | `/models` | Modelli disponibili (via `pi --list-models`) per la tendina FE | +| `POST` | `/sessions` | Crea sessione: `{workspace, question, provider?, model?, thinking?, name?}` → `{id}`. Backend pre-crea + spawn Pi (BE-5/BE-6) | +| `GET` | `/sessions` | Lista sessioni (da disco, via `tht session list --json` o FS) | +| `GET` | `/sessions/:id` | Manifest + fase derivata (via `tht … --json`) | +| `GET` | `/sessions/:id/artifacts/*` | Artefatti (schema_linking, ctes, sql_final, …) via `tht --json`/FS | +| `GET` | `/sessions/:id/events` | **SSE**: stream eventi (`text_delta`, `ui_request`, `info`, `system_event`, lifecycle). Re-emit del widget pendente alla (ri)sottoscrizione (BE-3) | +| `POST` | `/sessions/:id/response` | Invia `ui_response` → `extension_ui_response` su stdin Pi | +| `POST` | `/sessions/:id/steer` | Testo libero (canale steering `!`, architettura §4.3) → stdin Pi | +| `POST` | `/sessions/:id/sql/preview` | `?limit=&offset=` → delega `tht sql preview --json` → AGGrid (BE-2) | +| `POST` | `/sessions/:id/sql/export` | Delega `tht sql export` → CSV (BE-2) | +| `POST` | `/sessions/:id/close` | Teardown del processo Pi (la sessione su disco resta) | + +**Auth (D6):** middleware pluggabile `none` (utente `dev@local`, **primaria nell'MVP B**) | `mock` (utente statico da header, test) | `oidc` (evoluzione ad A). L'utente autenticato alimenta il campo `author` alla creazione sessione. Una sola codepath. + +--- + +## 5. Componenti interni + +1. **PiProcessManager** — ciclo di vita dei processi Pi: spawn (new/resume) con i flag corretti (`--mode rpc`, `--approve`, `--provider/--model/--thinking/--name`, `--session-dir`/`--session-id`), garanzia che `tht` sia nel **PATH del child** (fix bug #1 dell'L2), teardown alla chiusura/idle, **cap di sicurezza** sul numero di processi concorrenti, timeout di spawn. Al resume rilegge provider/model/thinking dal manifest e respawna con gli stessi. +2. **RpcClient** (uno per Pi) — `LineSplitter` **LF-only** su stdout (no `readline`, split su `\n`), parse JSONL, dispatch per `id`, scrittura JSONL su stdin. Framing derivato dal `RpcClient`/`LineSplitter` di riferimento (rischio fake-Pi: il fake deve riprodurre fedelmente questo framing). +3. **SessionBridge** (traduttore) — `extension_ui_request → ui_request`, `ui_response → extension_ui_response`; passthrough di `info`/`text_delta`/`system_event`. Mantiene la `ui_request` pendente per sessione (re-emit, BE-3). +4. **SSE hub** — uno stream per sessione; re-emit del widget pendente alla (ri)sottoscrizione. +5. **ThtRunner** — wrapper per le invocazioni `tht … --json` (session new/list/show, sql preview/export, artifacts): gestione subprocess, parsing JSON, mappatura degli exit-code del CLI. +6. **Auth middleware** — `none`/`mock`/`oidc` (D6). +7. **Config** — posizione dell'harness, dir workspace, profilo, porta, default per-sessione (provider/model/thinking), cap processi, timeout. + +--- + +## 6. Strategia di test (BE-4) + +- **L1 unit (TS):** logica pura di SessionBridge (traduzione widget-descriptor ↔ RPC), correlazione per `id`, parsing degli exit-code di `tht`, mappatura auth. Nessun subprocess. +- **L1 integrazione (fake-Pi):** il backend fa spawn del **fake-Pi** (asset condiviso) e si verifica il bridge reale: spawn, stdio, framing LF-only, correlazione, re-emit del pendente, sequenze multi-widget. Deterministico, CI-friendly. +- **L2 (informativo):** end-to-end contro `pi --mode rpc` reale, separato, non in CI (LLM non-deterministico, richiede VPN/credenziali). Coerente con lo split L0/L1/L2 dell'harness. + +Il fake-Pi è progettato per essere riusato dai golden test dell'harness (D10), così esiste un'unica fonte di fedeltà del protocollo. + +--- + +## 7. Dipendenze verso l'harness + +Il bridge non chiude il loop senza queste modifiche/verifiche lato `harness/`, da far atterrare prima o insieme al backend: + +1. **`tht sql preview`**: output `--json` strutturato + supporto `--offset` (paginazione AGGrid). *(Verificare lo stato attuale: oggi `preview` ha `--limit` ma non `--offset`/`--json` esplicito.)* +2. **Kickoff/gate**: accettare un **session id fornito** invece di farlo creare al modello (BE-5). +3. **`session_manifest.yaml`**: nuovi campi `provider`, `model`, `thinking`, `name` (BE-6/BE-7), oltre ai campi ThothII già previsti (`author`, ecc.). +4. **`.pi/settings.json`**: `quietStartup: true` + `trust` configurato per i file project-local (BE-7). +5. **`tht session list/show --json`**: per la lista/dettaglio sessioni nel FE (verificare se già presente). +6. **fake-Pi condiviso**: asset di test (BE-4), colma anche il gap D10. + +--- + +## 8. Fuori scope (MVP) + +- Web app centrale multi-utente (modello A, D12): l'MVP è B (localhost). L'evoluzione ad A riposiziona backend+harness su server e attiva OIDC senza cambiare i contratti. +- Concorrenza multi-utente reale (l'MVP è mono-operatore; il cap processi è solo una salvaguardia). +- Job runner asincrono: preview/export sono **sincroni** (coerente con l'architettura §9). +- CRUD workspace via API (i workspace sono YAML, scrittura manuale; il backend li espone in lettura — D3). +- Event-log/replay SSE con Last-Event-ID (BE-3 sceglie il re-emit del pendente). +- Cambio di model/thinking a sessione in corso (BE-6: solo alla creazione). +- Worker `tht` persistente per azzerare l'overhead di delega (BE-2: si valuta solo se i ~190 ms/chiamata diventano un problema reale). + +--- + +## 9. Rischi aperti + +- **PATH di `tht` nel processo Pi spawnato** (bug #1 dell'L2): il gate chiama `execFileSync("tht", …)`; il `tht` del venv deve essere raggiungibile dall'ambiente del child. Mitigazione: il PiProcessManager imposta `PATH`/usa path assoluto. +- **Trust dei file project-local** (BE-7): se Pi mostra un prompt di trust per la gate extension/skill, in RPC mode il loop si appende. Mitigazione: `--approve`/`trust` pre-configurato; da verificare empiricamente al primo spawn. +- **Versione di Pi** (`@earendil-works/pi-coding-agent`): il protocollo RPC e i flag possono cambiare. Mitigazione: pinnare la versione e documentarla. +- **Fedeltà del fake-Pi** (rischio ereditato dall'architettura): se il fake devia dal framing reale (LF-only, JSONL), i golden test non catturano regressioni reali. Mitigazione: basarlo sul `RpcClient`/`LineSplitter` di riferimento. +- **Dipendenze harness (§7) incomplete**: senza `preview --json/--offset` e l'id iniettato nel kickoff il loop non chiude. Mitigazione: trattarle come prerequisiti espliciti del piano. + +--- + +## 10. Nota sul piano di implementazione + +Questo documento è il design del backend. La fase di `writing-plans` produrrà il **piano di implementazione del backend** (uno, come da §11 dell'architettura), costruito a slice incrementali: prima il bridge RPC + SSE + REST minimale per chiudere il loop **F1 end-to-end** (con fake-Pi), poi steering, artifacts, delega SQL (preview/export), auth e i settings Pi. I contratti FE↔BE definiti qui (§4) e il widget-descriptor dell'architettura (§4) permettono di implementare e testare il backend in isolamento contro il contratto.