diff --git a/docs/superpowers/specs/2026-06-27-cli-port-completo-skill-riscritta-design.md b/docs/superpowers/specs/2026-06-27-cli-port-completo-skill-riscritta-design.md new file mode 100644 index 00000000..7577f302 --- /dev/null +++ b/docs/superpowers/specs/2026-06-27-cli-port-completo-skill-riscritta-design.md @@ -0,0 +1,223 @@ +# Design: porting CLI completo + riscrittura skill `nsp-sessione` + +**Data:** 2026-06-27 +**Stato:** bozza, in attesa di approvazione +**Contesto:** il piano harness (A8–D6) ha portato `nsp phase meta` (1/12 comandi CLI) e i modelli dati/backend core, ma il loop skill→LLM→gate resta non esercitato perché mancano gli altri 11 comandi CLI + la skill `nsp-sessione`. Questo design porta il resto per rendere il loop testabile end-to-end. + +## Decisioni (approvate in brainstorming) + +1. **Scope**: F1→F8 completo. Tutti gli 11 cmd CLI mancanti + i 3 cluster backend mancanti + skill riscritta. +2. **Drift phase.py**: riscrittura diretta sui 12 siti cmd che usano le vecchie costanti (`MAX_PHASE`/`PHASE_NAMES`/`SCHEMA_LINKING_PHASE`/`DECISION_MIN_PHASE`) → `load_workflow()` + metodi su `Workflow`. Niente strato wrapper. +3. **Skill**: riscrittura completa ex-novo che riflette l'architettura ThothII (widget-descriptor, D11 save-one, D13 free-text, D14 value-grounding/formula, D15 rollback), prendendo spunto dalla struttura ChironeWp3 ma non copiandola. + +## Sezione 1 — Architettura e strategia di porting + +Porting fedele con riscritture chirurgiche del drift, in ordine topologico (dipendenze radice prima), così ogni passo ha un'app che carica. Cinque onde: + +``` +Onda 0 — backend mancanti (foglie) + vendor/thoth_lsh, lshindex/, sqlcheck/, execute/, rest/execute.py, + rest/explain.py, ctetest.py, report.py, datamart.py(stub) + +Onda 1 — radici intra-CLI (espongono helper importati da tutti) + config_cmd (CONFIG_OPT), schema_cmd (_load_config_or_exit, physical_path, + annotations_path), session_cmd (session_dir, load_session_or_exit) + + require_phase_or_exit riscritta ex-novo in phase_cmd + + comandi advance/reopen/show di phase_cmd (oggi c'è solo meta) + +Onda 2 — vector layer + vector_cmd (make_embedder, open_store, open_searcher, require_vector_cfg) + +Onda 3 — cmd foglia (usano radici + vector) + memory_cmd, search_cmd, evidence_cmd, db_cmd, decision_cmd + +Onda 4 — cmd SQL/CTE (cluster sql_cmd) + sql_cmd (do_run, do_explain, promoted_tables_for, _load_physical_or_exit) + cte_cmd, datamart_cmd(stub), lsh_cmd (usa lshindex Onda 0) +``` + +Dopo ogni onda: `pytest` verde (109, nessuna regressione) + import smoke (`nsp --help` esce 0 per ogni nuovo sottocomando). + +## Sezione 2 — Porting CLI e drift (dettaglio) + +### Onda 0 — backend mancanti + +Tutti piccoli, nessun drift (non toccano phase/workflow). Port verbatim con rename `psdwp3→nsp`: + +| Modulo | Righe | Dipendenze | Note | +|---|---|---|---| +| `vendor/thoth_lsh.py` + `VENDORED.md` | ~85 | datasketch, tqdm (leaf puro) | copia in `nsp/vendor/` | +| `lshindex/__init__.py` | ~85 | vendor + `LshConfig` (portato) | `sed` rename | +| `sqlcheck/__init__.py` | ~125 | `ExecutionConfig` + `mschema.models` (portati) | leaf | +| `execute/__init__.py` + `execute/warnings.py` | ~150 | `ExecutionConfig` (portato) | leaf | +| `rest/execute.py` + `rest/explain.py` | ~60 | `execute` + `rest.client` (portato) | | +| `ctetest.py` | ~110 | sqlglot, pydantic | leaf | +| `report.py` | ~80 | execute + sqlcheck (stessa onda) | | +| `datamart.py` | ~30 | — | stub `raise NotImplementedError` | + +### Onda 1 — radici intra-CLI + `require_phase_or_exit` + +I 3 cmd radice si portano verbatim (rename), **eccetto** gli import di `session.phase` → `phase`. Poi `require_phase_or_exit` riscritta ex-novo: + +```python +# phase_cmd.py — aggiunta a quanto già portato (meta) +def require_phase_or_exit(cfg, session: str, min_phase: int) -> None: + from nsp.workflow import load_workflow + cur = current_phase(session_dir(cfg, session)) + if cur < min_phase: + wf = load_workflow() + nome = wf.phase_name(min_phase) + typer.secho( + f"Impossibile: serve la Fase {min_phase} ({nome}), sessione '{session}' è alla Fase {cur}.", + fg=typer.colors.RED, err=True, + ) + raise typer.Exit(1) +``` + +In questa onda si portano anche i comandi `advance`/`reopen`/`show` di `phase_cmd` con lo stesso fix dei 2 drift (const → `load_workflow()`, import path). + +### Drift: i 12 siti di costanti phase (meccanici, stesso pattern) + +```python +# PRIMA (ChironeWp3) +from psdwp3.session.phase import MAX_PHASE, PHASE_NAMES, SCHEMA_LINKING_PHASE, DECISION_MIN_PHASE +... MAX_PHASE ... PHASE_NAMES.get(n) ... DECISION_MIN_PHASE.get(type, 1) ... cur < SCHEMA_LINKING_PHASE + +# DOPO (ThothII) +from nsp.workflow import load_workflow +wf = load_workflow() +... wf.max_phase ... wf.phase_name(n) ... wf.decision_min_phase(type) ... cur < wf.schema_linking_phase() +``` + +Siti: 5 file cmd (`phase_cmd`, `session_cmd`, `decision_cmd`, `cte_cmd`, `datamart_cmd`). + +**Gap da colmare in `Workflow`**: manca il metodo `schema_linking_phase()`. Da aggiungere a `nsp/workflow.py`: + +```python +def schema_linking_phase(self) -> int: + """La fase che produce schema_linking.json (artifacts_out). Default 5.""" + for p in self.phases: + if "schema_linking.json" in p.artifacts_out: + return p.num + return 5 +``` + +### Onde 2-4 — cmd foglia e SQL/CTE + +Port verbatim (rename + fix import path `session.phase→phase`). Nessun altro drift: `DecisionType` è generico via `get_args` (i 4 nuovi tipi ThothII si pickano automaticamente), i modelli si costruiscono da JSON (`model_validate`), i campi ThothII-added (`grounded_values`, `concept_formulas`, `author/summary`) sono opzionali con default. + +**Registrazione nell'app**: alla fine di ogni onda, `app.add_typer(_app, name="")` in `cli/__init__.py`. Smoke verification: `nsp --help` esce 0. + +### Cross-cutting: ordine intra-CLI obbligato + +- `_load_config_or_exit`, `physical_path`, `annotations_path`, `session_dir`, `load_session_or_exit`, `CONFIG_OPT` sono definiti nei cmd radice (config_cmd/schema_cmd/session_cmd) e importati dalla maggior parte degli altri → portati per primi (Onda 1). +- `vector_cmd` exports (`make_embedder`, `open_store`, `open_searcher`, `require_vector_cfg`) consumati da memory_cmd/search_cmd/evidence_cmd → vector_cmd in Onda 2, prima di quei tre (Onda 3). +- `sql_cmd` exports (`do_run`, `do_explain`, `promoted_tables_for`, `_load_physical_or_exit`) consumati da cte_cmd + session_cmd → sql_cmd (Onda 4) prima di cte_cmd. (session_cmd è radice in Onda 1 ma i suoi path SQL dipendono da sql_cmd; verificare in implementazione se session_cmd va spostato dopo sql_cmd, o se i due path si disaccoppiano.) + +## Sezione 3 — Riscrittura skill `nsp-sessione` + +Riscrittura ex-novo che prende spunto dalla struttura ChironeWp3 ma riflette ThothII. Posizionamento: `harness/.pi/skills/nsp-sessione/` con `SKILL.md` + 4 sottomoduli (`cte.md`, `memoria.md`, `rewriting.md`, `sql-generation.md`). + +### Cosa si conserva dall'originale (valido, non reinventare) + +- Struttura a 8 fasi F1→F8 con prerequisito/postcondizione di ciascuna +- Discipline trasversali (un fatto per comando nsp, una domanda alla volta, accetta-la-proposta sempre fra le opzioni, nessuno step in limbo, artefatto = output di prima classe) +- Modello decisionale: `reviewer_decide` per decisioni descrittive (la scelta È la conferma), `reviewer_confirm` solo per i gate F1/F5/F7 +- Semantica dei comandi `nsp` (quando chiamarli, in che ordine) — ogni vincolo preciso (es. "max 5 memorie candidate, solo 3 tipi riusabili: concept_clarified/table_promoted/table_excluded") va trasferito fedelmente leggendo l'originale riga per riga, non parafrasato + +### Cosa si riscrive per riflettere ThothII + +1. **Modello gate → widget-descriptor.** La prosa originale dice "dialog native / checklist a caselle". Riscritta in termini di widget: `reviewer_select` (single-pick), `reviewer_decide` (multiselect con payload decisione), `reviewer_confirm` (artifact-gate). Le invarianti (Altro sempre presente, no-limbo, recommended marker) sono garantite dai builder (C1). La disciplina resta identica, il vocabolario cambia. + +2. **Fase 2 — save-one (D11) invece di full resync.** Originale: `memory promote` poi `memory index` (full resync, solo-server). ThothII: in `profile=workstation`, la promozione di una singola memoria usa `nsp memory save-one` (upsert mirato via writer key, implementato in B2); `memory promote + memory index` (full resync) resta per `profile=server`. La skill guida il modello al comando corretto: presenta `memory save-one` quando il contesto è una singola memoria promossa in workstation, `memory promote + memory index` quando si fa promozione batch lato server. La discriminante è il comando invocato (la CLI legge il profilo da `.env`); la skill non deve far dedurre il profilo al modello, ma scegliere il comando in base al numero di memorie da promuovere (una → save-one, molte → promote). Onesta sul limite: save-one ha cleanup distruttivo assente (resta server-side). + +3. **Fase 3 — riscrittura (invariata nel modello, solo vocabolario gate).** Prerequisito "devi essere già in Fase 3" (exit 5, `nsp/phase.py:171`), `question_rewritten` come decisione, sequenza ordinata (decisione → `rewrite_question` tool → `nsp session set-question` scrive `question.md` deterministicamente, `session/store.py:set_question`). Allineamento widget-descriptor. Gestione modifica/rifiuto invariata ("Altro" itera, "Torna indietro" riapre F1). + +4. **Fase 4 — value grounding (D14a) + formula (D14b).** Originale F4 menziona solo `table_promoted/excluded/column_corrected`. ThothII aggiunge: + - `value_grounded`: quando un valore citato (es. "ablazione") matcha **più colonne** (flag + testo), il modello presenta `reviewer_decide` con opzioni `value_grounded` per ogni colonna candidata (usando `aggregate_lsh_multi` che non collassa al miglior match). Il reviewer sceglie l'ancora. + - `concept_formula_approved/rejected`: quando un concetto (es. "fascia pediatrica", "stesso anno") ha una formula SQL candidata (`retrieve_formula`), il modello la presenta e il reviewer approva/rifiuta. Per la domanda di test, candidati naturali: estrazione anno da data, calcolo età nell'anno dell'ablazione, predicato same-year fra due eventi. + +5. **Discipline — free-text (D13).** Aggiunta: quando il reviewer usa "Altro" con testo libero, il modello valuta il testo in contesto, agisce, ri-chiede se ambiguo (non defaulta). Il testo registrato nel `rationale` della decisione (contratto testato in B5). + +6. **Discipline — rollback (D15).** Aggiunta: dopo `/torna N` o "Torna indietro", il modello riprende dalla fase N rivedendo gli artefatti esistenti; `teardown_to_phase` cancella gli artefatti oltre il target. Il modello NON ri-esegue comandi nsp per artefatti ancora validi. + +7. **Fase 8 — datamart resta stub.** `datamart.py` è `raise NotImplementedError`. La skill lo guida come nell'originale (decisione sì/no + hook stub), onesta sul fatto che non genera nulla. + +### Sottomoduli + +`cte.md`, `memoria.md`, `rewriting.md`, `sql-generation.md` portati adattandone la prosa gate (stessa logica del punto 1 sopra), mantenendo la sostanza tecnica. + +### Frontmatter + +`name: nsp-sessione`, `description` riscritta per ThothII (NL→SQL su Chirone, fasi 1-8, widget-descriptor gate, evoluzioni D11/D13/D14/D15). + +### Metodo di scrittura (per preservare l'intento originale) + +L'intento preciso di ogni fase (vincoli come "max 5 memorie candidate", "solo 3 tipi riusabili", "checklist pre-selezionata con la proposta", sequenze ordinate) si trasferisce leggendo l'originale riga per riga durante l'implementazione, non parafrasando. Se durante la scrittura un vincolo risulta poco chiaro, si chiede invece di indovinare. + +## Sezione 4 — Testing e criteri di successo + +### Test automatici (CI-runnabile, a ogni onda) + +**Porting CLI + backend (Onde 0-4):** +- Dopo ogni onda, `pytest` (L0+L1) deve restare **109 passed** — nessuna regressione. +- Import smoke per onda: `python -c "from nsp. import ..."` per ogni nuovo backend; `nsp --help` esce 0 per ogni nuovo sottocomando. +- Drift L1 (nuovo test): esercita `require_phase_or_exit` su fasi sintetiche — conferma che la riscrittura contro `Workflow` funziona. Pure logic, `tmp_path`, no DB. + +**Skill:** +- Non unit-testable in L1 (prosa per il modello). +- Verifica automatica: `grep -c "PsdWp3\|psdwp3"` in `harness/.pi/skills/nsp-sessione/` = 0; ogni `nsp ` citato corrisponde a un sottocomando registrato in `cli/__init__.py` (script di coerenza). + +### Test L2 — la sessione di test (manuale, pre-rilascio) + +Domanda di test (complessa, massima copertura): + +> Crea una lista con i pazienti che negli ultimi 20 anni hanno avuto una cardioversione elettrica ed una ablazione lo stesso anno. Per ogni paziente incluso nella lista esponi il sesso, l'età che aveva il paziente nell'anno in cui ha fatto l'ablazione, tutti i dati rilevanti dell'ablazione e tutti i dati rilevanti della cardioversione. + +Verifica sullo schema reale: entrambi i concetti hanno fact table dedicate (`fact_cardioversione_elettrica_transtoracica`, `fact_studio_elettrofisiologico_endocavitario_ablazione` + ~13 `fact_see_ablazione_*` di dettaglio). La domanda esercita F4 complesso (2 fact + ~15 dim/detail, join multipli), D14 (value-grounding: "ablazione"/"cardioversione elettrica" matchano nomi-tabella e campi/patologie; formula: "stesso anno", "età nell'anno dell'ablazione"), F6 (decomposizione CTE naturale), F7 (SQL finale complesso). + +**Criteri di successo della sessione:** + +1. **Avvio**: `/nuova-domanda ""` → il modello chiama `nsp session new`, legge la skill, inizia F1. +2. **Gate funziona**: il modello chiama i tool `reviewer_*` e il gate presenta widget-descriptor (non prosa grezza). Il reviewer può rispondere. +3. **Persistenza**: ogni decisione del reviewer produce una riga in `review_decisions.jsonl` (verificabile leggendo il file dopo). +4. **Avanzamento fasi**: il workflow avanza F1→... secondo le decisioni, usando i comandi `nsp` corretti (non `phase advance` manuale, non `decision add` diretto). +5. **Anti-bypass**: se il modello prova `nsp phase advance` da shell, il gate lo blocca (hook `tool_call`). +6. **Artefatti**: per una domanda che arriva in fondo, `schema_linking.json` + `cte_plan.json`/`ctes/*.sql` + `sql_final.sql` prodotti e coerenti col ledger. + +**Copertura per evoluzione:** + +| Evoluzione | Esercitata? | Come | +|---|---|---| +| F1/F3/F5 (base) | sì | chiarimento, riscrittura, sintesi | +| F4 complesso | sì | 2 fact + ~15 dim, join multipli | +| D14a value-grounding | sì | "ablazione"/"cardioversione" matchano tabella + campi | +| D14b formula | sì | "stesso anno", "età nell'anno ablazione" | +| D13 free-text | se innesca | se il reviewer corregge via "Altro" | +| D11 save-one (F2) | solo se memorie trovate | se `nsp memory search` in F2 trova match (NOTA: tabella memory azzerata di recente — la prima sessione probabilmente salta F2 senza match, quindi D11 va validato con una seconda sessione dopo aver popolato le memorie) | +| F6 CTE | sì | decomposizione naturale | +| D15 rollback | se innesca | se il reviewer torna indietro | + +### Onestà + +- Più copertura = più punti di rottura. Una domanda complessa ha più probabilità di fallire a metà in punti che una semplice non tocca. Un fallimento a metà **non è un fallimento del porting** — è il segnale che L2 è designed a cogliere. Va letto come tale. +- Durata: sessione lunga (più decisioni, CTE uno a uno). Va bene per test pre-rilascio, non per ripeterlo spesso. +- Gate glue in CI: resta verificato solo nella sessione L2 manuale senza un fake-Pi mock (follow-up cross-cutting noto). +- Comportamento non deterministico di GLM 5.2: la sessione può andare diversamente a ogni run. L2 è safety-net pre-rilascio, non regression gate. + +### Ordine di esecuzione e definition of done + +1. Onda 0 (backend) → `pytest` verde + import smoke +2. Onda 1 (radici CLI + require_phase_or_exit + phase advance/reopen/show) → `pytest` verde + `nsp phase advance --help` esce 0 +3. Onda 2-4 (cmd foglia + SQL/CTE) → `pytest` verde + `nsp --help` mostra tutti i sottocomandi +4. Skill riscritta → script di coerenza (no PsdWp3, cmd citati = cmd registrati) +5. Sessione L2 (manuale, con l'operatore) → criteri 1-6 sopra + +**Definition of done**: Onde 0-4 + skill completate con `pytest` verde + sessione L2 che soddisfa i criteri 1-4 (avvio, gate, persistenza, avanzamento). I criteri 5-6 (anti-bypass, artefatti finali) sono verification aggiuntiva nella stessa sessione. + +## Fuori scope (rimane aperto) + +- **Gate glue in CI** (fake-Pi mock): follow-up cross-cutting. Il glue `nsp-gate.js` resta verificato solo in L2 manuale. +- **L2 come regression gate**: resta pre-rilascio, non CI. +- **`nsp lsh build` completo** (port dell'indice LSH da schema reale): il test L2 value-grounding (`test_value_grounding_real`) richiede un indice buildato; `nsp lsh build` si porta qui (lsh_cmd in Onda 4), ma la build dell'indice sul workspace chirone-test è un'operazione pre-sessione separata. +- **RPC mancanti lato server** (`validate_select`, `current_user`): rilevate 404 nei test. Non bloccano il loop base, ma vanno (ri)installate lato Supabase per i path pre-SQL.