# PRD — Modularizzazione a moduli incapsulati: disambiguazione, evidence, memory **Status:** riflessione architetturale di plausibilità — nessuna decisione implementativa presa qui; il documento fissa confini, contratti condivisi e precondizioni da cui potranno nascere ticket e piani separati **Data:** 2026-08-21 **Autore:** analisi del codice su main (`5f05754`), su richiesta del proprietario; incrocia e non sostituisce i documenti evidence-canonica già esistenti (sez. 9) **Uso:** riferimento per il programma di sviluppo parallelo dei tre sottosistemi (disambiguazione, gestione evidence, gestione memory); definisce cosa deve essere vero *prima* che i tre flussi possano girare in parallelo senza impatti incrociati --- ## 1. L'obiettivo del proprietario Avviare uno sviluppo che cambi **in parallelo** tre aree del sistema: 1. **disambiguazione** (estratta e separata dal workflow); 2. **gestione delle evidence** — nuovo componente che espone un'API verso il resto del workflow ma resta ben incapsulato al suo interno; 3. **gestione della memory** — con lo stesso grado di forte incapsulamento. Vincolo accettato: le modifiche fatte su un modulo **non devono impattare gli altri moduli** se non per eventuali ridefinizioni dei contratti (API) esposti. ## 2. Verdetto sintetico **Plausibile, e il codebase è insolitamente ben predisposto per farlo.** Ma con tre precisazioni oneste: 1. i tre componenti sono oggi a **tre livelli di maturità diversi**: evidence è quasi un modulo, memory è già incapsulata meglio di quanto sembri, la disambiguazione **non è un componente di codice** e "estrarla" è un lavoro di natura diversa (sez. 4); 2. esistono **due "quinti moduli" nascosti** — il ledger dei tipi di decisione con `workflow.yaml`, e il gate `tht-gate.js` con il vector store — che il piano a tre vie deve contemplare, altrimenti il parallelismo si rompe dove non si sta guardando (ledger e gate sono spiegati in sez. 3; le implicazioni in sez. 5); 3. gli accoppiamenti reali **non sono negli import Python**: sono nei decision type del ledger, nelle proiezioni `search pack`/`search find`, nel modello di contenuto della memory e negli id evidence citabili. Sono quelli i contratti da definire e versionare **prima** del parallelismo (sez. 6–7). ## 3. Ledger e gate: i due elementi cardine del workflow Il resto del documento usa di continuo due concetti che conviene fissare qui, perché sono i punti dove i tre moduli si incontrano davvero: il **ledger** (dove lo stato viene registrato) e il **gate** (chi decide e chi scrive). ### 3.1 Il ledger — il registro delle decisioni (`review_decisions.jsonl`) Una sessione è una directory: manifest + artefatti per fase (`question.md`, `schema_linking.json`, `sql_final.sql`, …) + un file `review_decisions.jsonl`. Il ledger è **append-only**: una riga JSON per ogni decisione del reviewer, con numero progressivo `seq`, timestamp, `type`, `subject`, `detail`, `rationale` (più i campi di rollback `retracts`/`phase`). I `DecisionType` definiti in `tht/decisions.py` sono esattamente 30. Esempio concreto: a F1 il reviewer sceglie "per 'ablazione' intendo la colonna `flag_ablazione` della fact delle procedure" → nel ledger viene accodata una riga come `{"seq": 3, "ts": "…", "type": "concept_clarified", "subject": "ablazione", "detail": "…", "rationale": "…"}`. Due proprietà lo rendono cardine: 1. **la fase corrente non è memorizzata: è calcolata** rileggendo (fold) il ledger (`tht/phase.py`). Il ledger è la fonte di verità — "ciò che non è registrato non è accaduto". Il rollback non cancella nulla: aggiunge `decision_retracted`/ `phase_reopened` e la vista *effettiva* (`effective_decisions`) esclude le righe ritirate e quelle rimaste stale oltre il punto di rollback; 2. **è il canale di comunicazione tra i moduli**: la disambiguazione *scrive* `concept_clarified`/`ambiguity_open`/`value_grounded`; l'evidence *scrive* `evidence_accepted`/`evidence_rejected`/`concept_formula_approved|rejected`; la memory *legge* `concept_clarified` (per costruire le promozioni) e *scrive* `memory_promoted`/ `memory_promotion_declined`. Per questo l'insieme dei decision type **è** un contratto: ridefinirlo (sez. 6.1) è l'equivalente di cambiare una API tra moduli. ### 3.2 Il gate — l'estensione che impone il human-in-the-loop (`tht-gate.js`) ThothII non esegue il workflow con codice: lo esegue **un agente LLM (Pi)** che segue SKILL.md. Il modello propone, ma non decide e non persiste direttamente. Il gate è l'estensione JavaScript di Pi (~2000 righe, `harness/.pi/extensions/tht-gate.js`) che svolge tre lavori: 1. **traduce le proposte del modello in widget per l'umano**: il modello chiama un tool del gate — `reviewer_select` (scelta singola), `reviewer_decide` (multiselect), `reviewer_confirm` (gate su artefatto/fase) più i specializzati `rewrite_question`, `reviewer_schema_linking`, `reviewer_datamart`, `reviewer_memory_promote`, `write_schema_linking`, `write_cte_sql`, `write_final_sql`; il gate costruisce il descriptor del widget → RPC → backend → SSE → browser, dove il frontend lo renderizza dal registry `src/widgets/`; 2. **persiste attraverso un unico canale**: alla risposta del reviewer, è il gate (non il modello) a invocare la CLI `tht` (es. `tht memory promote --preview --json`, `tht session sync-schema-linking`) e ad accodare la riga nel ledger; 3. **fa rispettare le regole (enforcement)**: l'hook anti-bypass blocca `tht phase advance`, `tht decision add`, `tht memory promote|save-one` se il modello prova a eseguirli direttamente dalla shell; i payload vengono validati (TypeBox) e gli artefatti mostrati al reviewer sono ricostruiti in modo deterministico (es. il widget CTE mostra il SQL persistito via `tht cte info`, non il testo del modello); le chiusure di fase deterministiche sono automatiche. Esempio concreto (F8): il modello chiama `reviewer_memory_promote`; il gate esegue `tht memory promote --preview --json`, costruisce la checklist pre-selezionata; il reviewer conferma; il gate salva le memory scelte (`memory save-one`), registra `memory_promoted`/`memory_promotion_declined` nel ledger e finalizza la sessione. Il modello non ha mai scritto nulla di propria mano. Perché è cardine per la modularizzazione: il gate è **l'unico punto** in cui i tre moduli appaiono all'utente e in cui le loro regole vengono imposte — ogni cambiamento a disambiguazione/evidence/memory ha quasi sempre una controparte nel gate (sez. 5.2). ## 4. Stato attuale dei tre componenti ### 4.1 Evidence — il più vicino a essere un modulo Fatti verificati nel codice: - esiste già un **port vero** (`harness/tht/ports/evidence.py`: `discover`/`acquire`, credential-free, contratti Pydantic congelati) con **tre adapter** dietro il port (`adapters/evidence/{filesystem,http,s3}.py`); - la pipeline di canonizzazione/indicizzazione vive in `harness/tht/corpus/` (sorgente → normalize → chunk → embed → vector store, generazioni isolate stage→publish); - la **superficie di consumo del workflow è già quasi tutta incanalata in due comandi con JSON pulito**: `tht search pack` (sezione evidence: top 5, proiezione `{title, status, excerpt[:400]}`) e `tht search find [--kind evidence|formula]`. Quella proiezione è già di fatto un'API: evidence-canonica può rifare tutto dietro di essa (card, involucro tipizzato, retrieval duale) senza che F1 se ne accorga, finché il contratto di proiezione regge; - l'**inventario dei punti di consumo è già fatto**: `verifica-workflow.md` elenca i 12 touchpoint (T1–T12) con meccanismo e requisito. Quel documento **è** la bozza del contratto esterno del modulo. Ciò che manca all'incapsulamento sono esattamente i tre GAP lì identificati: - **GAP 1 — identità citabile**: il processo tratta l'id evidence come documento (F3 lo cita, F4 lo attacca ai candidati, il report di sessione lo risolve in file via `resolve_evidence_file`/`rglob`). La decisione già presa (id citabile `doc#card`) richiede l'estensione del resolver: è l'ultimo passo di contratto da chiudere; - **GAP 2 — formula store**: lettura cablata e funzionante, scrittura senza proprietario di produzione. La decisione già presa (le card famiglia 3 del corpus canonico sono fonte di verità; i `.sql.md` sono derivati della pipeline) assegna il proprietario senza toccare il contratto di lettura D14b; - **GAP 3 — conoscenza di dominio nei testi istruzionali**: dialetto, ricette temporali PSD, esempi clinici dentro SKILL.md e reference doc. È contenuto evidence sparso nel workflow: la migrazione verso il corpus canonico **è parte del lavoro di confine**, non un'attività cosmetica successiva. Nota trasversale: il ciclo di vita evidence attraversa **due linguaggi** — materializzazione e ciclo di vita della collection stanno nel backend TypeScript (P4/P6), corpus e ricerca nello harness Python. L'API del modulo deve quindi essere il contratto CLI/JSON (che è già lo stile di casa: `--json` pristine), non un'interfaccia Python. ### 4.2 Memory — già incapsulata, con un accoppiamento semantico da esplicitare Fatti verificati: - il modulo vive in `harness/tht/memory.py` + `tht/solved.py` + `cli/memory_cmd.py`, con registro JSONL a scrittura atomica e dedup del contenuto reviewer-visibile; - i punti di contatto col workflow sono **esattamente tre**: F2 (ricerca + applicazione via gate), F8 (gate di promozione), finalize (indicizzazione `solved_question`, best-effort); - il gate consuma la memory **solo tramite CLI**: `tht memory promote --preview --json` e `memory save-one`; la logica di calcolo dei candidati (dedup, decisioni storie post-rollback, seq rifiutate, esclusione delle già decise nella sessione) è tutta dentro memory.py. È incapsulamento vero, già operativo. L'accoppiamento che conta non è di codice ma **semantico**: `REUSABLE_TYPES = {"concept_clarified"}` — il modello di contenuto della memory è definito dal tipo di output della disambiguazione. Se il ridisegno F1 cambia cosa sia un chiarimento (target group, tassonomia dell'ambiguità, caso unanswerable), cambia anche cosa la memory può promuovere e come. **Il contratto disambiguazione→memory va definito prima di far girare i due flussi in parallelo**, o si scontreranno esattamente lì. ### 4.3 Disambiguazione — non è un componente di codice Il punto onesto: **non c'è niente da "estrarre" nel senso classico**. Oggi la disambiguazione è il comportamento dell'LLM in F1/F3/F4, governato da (ledger e gate: sez. 3): - **prosa** (SKILL.md + `rewriting.md`): una sola ambiguità per volta, opzioni informate, "lascia aperta l'ambiguità" esplicita; - **retrieval**: pack iniettato (`retrieval_pack.md`), `tht search find`, `tht schema render`, LSH con `aggregate_lsh_multi` che espone *tutte* le colonne dove un valore appare (es. "ablazione" su flag booleano + campo patologia testuale); - **ledger**: `concept_clarified`, `ambiguity_open`, `value_grounded`, `question_rewritten`; - **gate**: i widget che presentano le interpretazioni candidate. "Extractorla" significa quindi, in ordine: 1. **definire il suo contratto input/output** — in: domanda + contesto di retrieval; out: ambiguità con target group, opzioni candidate, segnale unanswerable distinto dall'ambiguo. È ciò che `impact-ridisegno-disambiguazione.md` §3 già elenca (retrieval duale lessicale+semantico, target group, unanswerable); 2. **darle una superficie** che l'orchestratore consuma (sezione del pack, comando, sezione skill dedicata) invece di comportamento libero del modello; 3. **spostare la conoscenza che usa** fuori da SKILL.md e dentro le evidence (GAP 3). Rischio specifico e strutturale: l'LLM è colla che **può bypassare qualunque contratto su carta**. In questo codebase l'unico enforcement che funziona è a livello gate/CLI — il pattern è già dimostrato dall'hook anti-bypass che blocca `tht phase advance`/`decision add` da shell e `tht memory promote`/ `save-one` fuori dal gate. Finché il contratto di disambiguazione non è enforcement (hook + CLI + skill come contratto completo), la proprietà "le modifiche non impattano altri moduli" è una speranza, non una proprietà verificabile. ## 5. I due quinti moduli nascosti ### 5.1 Il ledger + `workflow.yaml` — la colonna vertebrale condivisa Tutti e tre i moduli leggono/scrivono il decision ledger (`review_decisions.jsonl`, sez. 3.1): la disambiguazione emette `concept_clarified`/`value_grounded`; l'evidence emette `evidence_accepted`/`evidence_rejected`/ `concept_formula_approved|rejected`; la memory legge `concept_clarified` ed emette `memory_*`. Il campo `emits` di `workflow.yaml` accoppia fasi e decision type; `phase.py` piega il ledger per calcolare la fase corrente. Questo è un **bene**: il ledger è già un contratto a eventi tra i moduli. Ma è superficie condivisa — tutti e tre i flussi vorranno toccarla. Va trattata come contratto a sé (quarto), con controllo dei cambiamenti, non come territorio libero. ### 5.2 Il gate (`tht-gate.js`) e il vector store — i punti caldi di merge - **Il gate** (sez. 3; ~2000 righe + builders/enrich/artifact-contracts) renderizza candidati evidence, dedup opzioni memory, costruisce il widget schema-linking con id evidence, guida F2/F8 memory e fa enforcement anti-bypass. **Tutti e tre i flussi avranno bisogno di modifiche al gate**: se non viene spaccato lungo gli stessi confini dei moduli, diventa il punto caldo di merge del programma parallelo. - **Il vector store**: una collection Qdrant per workspace, kind-separata (`schema_table`, `schema_column`, `evidence`, `memory`, `solved_question`); il contratto di collection e payload-index è di proprietà del backend TS (P4), lettura/scrittura sono Python. I record schema/evidence sono revision-scoped (P3), memory/solved workspace-wide. Se evidence-canonica cambia la forma dei record/payload (il frontmatter già fluisce libero nei metadata), gli assunti di indicizzazione della memory ne risentono. Il vector store va designato come **infrastruttura condivisa con contratto proprio**, non come territorio implicito di uno dei tre. ## 6. I contratti che sono le vere API Il parallelismo è protetto da quattro contratti, nessuno dei quali è un'interfaccia Python: 1. **Decision type del ledger** (`workflow.yaml` `emits` + `decisions.py`): l'interfaccia a eventi tra i tre moduli e il workflow. Ogni ridefinizione è una modifica di contratto, va versionata e comunicata; 2. **Proiezioni di consumo evidence** (`search pack` / `search find --kind evidence|formula`): la forma di ciò che F1/F3/F4 vedono. È il cuore del ticket "Consumo per fase", che già porta i requisiti della F1 ridisegnata e **non va congelato prima** che il disegno F1 abbia almeno uno schizzo; 3. **Modello di contenuto memory** (`REUSABLE_TYPES` + campi del record + proiezione `promote --preview --json`): dipende semanticamente dal contratto di output della disambiguazione (sez. 4.2); 4. **Id evidence citabile** (`doc#card` + estensione del resolver id→file): il contratto che tiene insieme F3 (citazioni), F4 (candidati), decisions (subject) e report di sessione. ## 7. Ordine di sviluppo consigliato Coerente con quanto già deciso in `map.md` e nell'impact doc: 1. **schizzo del disegno F1** della disambiguazione (anche non finale): target group, retrieval duale, unanswerable — perché il contratto di consumo va progettato *contro la F1 ridisegnata*; 2. **chiusura del contratto "Consumo per fase"** (cerniera tra evidence-canonica e F1): proiezioni, filtri, id citabili `doc#card` con resolver esteso; 3. **definizione del contratto disambiguazione→memory** (cosa è riusabile, con quali campi); 4. **solo allora** i tre flussi girano in parallelo, ciascuno nel suo worktree. Strumento di protezione del parallelismo già disponibile in casa: il **pattern dei port-contract test** (`test_evidence_port_contract.py`, `test_vector_port_contract.py`, `test_dwh_port_contract.py`). Estenderlo ai nuovi confini (proiezioni evidence, contratto memory, enforcement di disambiguazione) è il modo più economico per rendere l'indipendenza dei moduli **verificabile** invece che sperata: un flusso rompe l'altro se e solo se rompe un test di contratto, non per effetti nascosti. ## 8. Rischi e mitigazioni | Rischio | Natura | Mitigazione | |---|---|---| | L'LLM orchestratore bypassa il contratto di disambiguazione | strutturale (il workflow è LLM-driven) | enforcement gate/CLI sul modello dell'anti-bypass esistente; SKILL come contratto completo, non consiglio | | Collisione disambiguazione↔memory su `concept_clarified` | semantica | contratto esplicito (sez. 6.3) definito prima del parallelismo | | `tht-gate.js` punto caldo di merge | organizzativa | spaccare il gate lungo gli stessi confini dei moduli (builders per modulo) | | Deriva del contratto vector store tra TS e Python | infrastrutturale | contratto di collection/payload/kind di proprietà unica, test di contratto su entrambi i lati | | modifiche contagiose a `workflow.yaml`/ledger | processo | change control sul ledger come quarto contratto; `emits` data-driven già aiuta | | contratti congelati troppo presto (Consumo per fase) | sequenza | ordine sez. 7: prima schizzo F1, poi chiusura contratto | ## 9. Riferimenti - `docs/wayfinder/evidence-canonica/map.md` — mappa del sottoprogetto evidence-canonica (destinazione, decisioni chiuse, nebbie); - `docs/wayfinder/evidence-canonica/prototypes/verifica-workflow.md` — i 12 touchpoint evidence (T1–T12) e i GAP 1–3: l'inventario dell'interfaccia esterna del futuro modulo evidence; - `docs/wayfinder/evidence-canonica/prototypes/impact-ridisegno-disambiguazione.md` — valutazione d'impatto del ridisegno F1 (requisiti: retrieval duale, target group, unanswerable; coordinamento dei due progetti); - `docs/wayfinder/evidence-canonica/prototypes/PROTOTIPO-formato-unita-canonica.md` e `tipologia-evidence.md` — formato card+involucro chiuso il 2026-08-21 e casistica a 7 famiglie; - `docs/disambiguazione-iniziale.md` — stato attuale della disambiguazione; - `docs/gestione-memory.md` — disegno della gestione memory; - `harness/workflow.yaml`, `harness/.pi/skills/tht-sessione/SKILL.md`, `harness/.pi/extensions/tht-gate.js` — le fonti di codice citate (fasi, discipline, gate); - `docs/prd/2026-08-09-workspace-preprocessing-prd.md` — il PRD da cui nasce l'attuale catena preprocessing/registry (contesto P2–P6 citato in sez. 4.1 e 5.2).