Files
ThothII/docs/superpowers/specs/2026-06-27-backend-design.md
T
marcopanandClaude Opus 4.8 aad6299c99 docs(backend): design del backend ThothII (brainstorming)
Decisioni BE-1..BE-7: un Pi per sessione attiva, SQL finale delegato a tht
(codepath unico, rischio D7 eliminato), resilienza via ricostruzione da disco +
re-emit del widget pendente, test con fake-Pi condiviso, backend pre-crea la
sessione, model/thinking/provider per-sessione persistiti, settings Pi MVP.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 18:09:51 +02:00

190 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/<id>/ (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 `<id>` 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.