# Task 2 — Il questionario in pagina — report **Stato:** completato. 7/7 test nuovi verdi, suite completa allo stato atteso (1 fallito pre-esistente e fuori perimetro, 321 passati), `npx tsc --noEmit` pulito. Commit fatto. **Commit:** `f5ca841` — "longevity: il questionario in pagina, e il consenso che si puo negare davvero" File aggiunti/modificati (solo questi, `git add` esplicito, nessun `-A`/`.`): - `src/lib/longevity/vista.ts` (nuovo) — `campiDelQuestionario`, `salvaDalForm` - `src/lib/longevity/db.ts` (modificato) — `getLongevityDb`/`getIdentityDb`, connessioni singole per processo - `src/components/longevity/Questionario.tsx` (nuovo) — isola React, quattro blocchi - `src/pages/longevity/questionario.astro` (nuovo) — pagina, nessuna query diretta - `src/pages/api/longevity/questionario.ts` (nuovo) — endpoint POST - `src/styles/longevity.css` (modificato) — classi `.lg-form*` per il modulo - `tests/longevity/questionario-pagina.test.ts` (nuovo) — i 7 test del brief ## Procedura seguita (TDD) ### Step 1-2: test scritto, verificato rosso Comando: ``` npm test -- tests/longevity/questionario-pagina.test.ts ``` Esito (prima dell'implementazione): ``` Error: Cannot find module '../../src/lib/longevity/vista' imported from .../tests/longevity/questionario-pagina.test.ts Test Files 1 failed (1) Tests no tests ``` Come atteso: `vista.ts` non esisteva ancora. ### Step 3: implementazione **`src/lib/longevity/vista.ts`** - `campiDelQuestionario(db, alla)`: legge `testAttivi(db, alla)`, tiene solo i `test_id` col prefisso `q_` (per non confondersi coi test fisici), mappa in `{ id, etichetta, tipo, unita, min, max, sottoDominio }`. - `salvaDalForm(db, input)`: **controlla il consenso prima di scrivere qualunque cosa**. `consensoDato()` accetta solo `'si'`/`'sì'` (case/trim-insensitive) come affermativo — tutto il resto (`'No'`, stringa vuota, qualunque altra cosa) è un rifiuto. Se il consenso non è dato: **nessuna sessione si apre**, si scrive comunque una nota di consenso (`campo_id = 'consenso_sanitario'`, `sessione_id = null`, testo con l'esito letterale e `QUEST_VERSION`) e si ritorna `{ ok: false, errore: '...' }`. Se il consenso è dato: si delega a `salvaCompilazione` (già esistente, non toccata) e **poi** si scrive la nota di consenso legata alla sessione appena creata. **`src/lib/longevity/db.ts`** - Aggiunte `getLongevityDb()`/`getIdentityDb()`, singleton per processo (stesso pattern di `getDb()` in `src/lib/db.ts`). Necessarie perché l'endpoint deve risolvere il `client_code` dalla sessione (via `codicePerUtente`, che vuole una connessione a `identity.db`) e poi scrivere su `longevity.db`: senza un singleton ogni richiesta avrebbe aperto due connessioni SQLite nuove. - Nota di conformità al vincolo di `tests/longevity/anagrafica.test.ts` ("nessun file in `src/`, escluso `anagrafica.ts` e `db.ts`, apre insieme le due connessioni", verificato grep-ando le stringhe letterali `createIdentityDb`/`createLongevityDb`): l'endpoint importa `getIdentityDb`/`getLongevityDb` (definite in `db.ts`, già un file autorizzato), non le funzioni `create*` — il test infatti resta verde (vedi sotto). **`src/pages/api/longevity/questionario.ts`** - `codicePerUtente(getIdentityDb(), locals.user!.id)`: il codice cliente **si legge solo qui**, dalla sessione. Il corpo della richiesta non viene mai letto per questo campo — non esiste nel codice nessun `data.client_code`/`data.clientId`. - Se non c'è un fascicolo per l'utente loggato: 403, nessun salvataggio. - `liberi` è filtrato sulle sole chiavi di `CAMPI_LIBERI` (whitelist, non passa chiavi arbitrarie dal corpo). - La data della compilazione è quella del server (`new Date().toISOString().slice(0,10)`), non quella (manomettibile) inviata dal client. - Risposta: `201 { sessioneId }` se `salvaDalForm` va a buon fine, `422 { error }` altrimenti (consenso mancante/negato, o qualunque altro errore di `salvaDalForm`). **`src/components/longevity/Questionario.tsx` + `src/pages/longevity/questionario.astro`** - La pagina Astro chiama `campiDelQuestionario(getLongevityDb(), oggi)` e passa i campi all'isola: nessuna query SQL nel file della pagina. - L'isola mostra quattro blocchi in sequenza (Sonno & Recupero, Energia & Stress, Stile di Vita — ricostruiti filtrando `campi` per `sottoDominio`, dato che il registro li restituisce in ordine alfabetico di `test_id`, non raggruppati — più Note e consenso), con avanti/indietro e invio solo sull'ultimo blocco. - **Deliberato**: il consenso NON è bloccato lato client oltre al richiedere una scelta (Sì/No) — scegliere "No" e provare a inviare è permesso, esattamente perché la verifica che conta è quella server-side. Bloccare "No" anche lato client avrebbe solo spostato l'errore del prototipo in un altro file senza risolverlo. ### Step 4: test target verde Comando: ``` npm test -- tests/longevity/questionario-pagina.test.ts ``` Output: ``` Test Files 1 passed (1) Tests 7 passed (7) ``` ### Step 5: suite completa + typecheck Comando: ``` npm test ``` Output (coda): ``` ❯ tests/modifiche-agosto.test.ts (25 tests | 1 failed) 49ms × l'inaugurazione dice la data e che il programma manca ancora 11ms FAIL tests/modifiche-agosto.test.ts > promo ed eventi > l'inaugurazione dice la data e che il programma manca ancora AssertionError: expected 'Inaugurazione' to contain '19 settembre 2026' Test Files 1 failed | 38 passed (39) Tests 1 failed | 321 passed (322) ``` Esattamente il rosso pre-esistente dichiarato nel task (fuori perimetro, non toccato) — nessun altro test rotto, i 7 nuovi inclusi nel verde. In particolare `tests/longevity/anagrafica.test.ts` (il vincolo sulle due connessioni) resta verde: vedi nota su `getIdentityDb`/`getLongevityDb` sopra. Comando: ``` npx tsc --noEmit ``` Output: nessuno (exit pulito). ### Step 6: commit ``` git status --porcelain ``` ``` M src/lib/longevity/db.ts M src/styles/longevity.css ?? src/components/longevity/ ?? src/lib/longevity/vista.ts ?? src/pages/api/longevity/ ?? src/pages/longevity/questionario.astro ?? tests/longevity/questionario-pagina.test.ts ``` ``` git add src/lib/longevity/vista.ts src/lib/longevity/db.ts \ src/components/longevity/Questionario.tsx \ src/pages/longevity/questionario.astro \ src/pages/api/longevity/questionario.ts \ src/styles/longevity.css \ tests/longevity/questionario-pagina.test.ts git commit -m "longevity: il questionario in pagina, e il consenso che si puo negare davvero" ``` ``` [feat/longevity f5ca841] longevity: il questionario in pagina, e il consenso che si puo negare davvero 7 files changed, 482 insertions(+) ``` `git status` dopo il commit: albero pulito. ## La prova richiesta: rompere il controllo sul consenso Backup di `src/lib/longevity/vista.ts`, poi iniettata la regressione esatta descritta nel task — la stessa logica del prototipo del cliente, che blocca solo se il campo è vuoto: ```diff function consensoDato(risposta: string): boolean { - return CONSENSI_AFFERMATIVI.has(risposta.trim().toLowerCase()); + // REGRESSIONE DI PROVA: replica il bug del prototipo del cliente (blocca solo se vuoto). + return risposta.trim() !== ''; } ``` Comando: ``` npm test -- tests/longevity/questionario-pagina.test.ts ``` Output: ``` ❯ tests/longevity/questionario-pagina.test.ts (7 tests | 1 failed) 13ms × NON salva se il consenso e rifiutato: "No" non e un consenso 4ms FAIL tests/longevity/questionario-pagina.test.ts > il salvataggio, e il consenso > NON salva se il consenso e rifiutato: "No" non e un consenso AssertionError: expected true to be false // Object.is equality - false + true Test Files 1 failed (1) Tests 1 failed | 6 passed (7) ``` **Rosso, come atteso**: il test `NON salva se il consenso e rifiutato: "No" non e un consenso` intercetta esattamente la regressione — protegge la cosa giusta, non solo il nome di una funzione. Ripristinato il file dal backup (`diff` col backup: nessuna differenza), rilanciato: ``` npm test -- tests/longevity/questionario-pagina.test.ts ``` ``` Test Files 1 passed (1) Tests 7 passed (7) ``` Poi riverificati suite completa (`321 passed`, `1 failed` pre-esistente) e `npx tsc --noEmit` (pulito) allo stato finale. ## Le tre regole di sostanza — dove sono, e come si verificano 1. **Il consenso non affermativo blocca il salvataggio.** `salvaDalForm` chiama `consensoDato()` **prima** di ogni scrittura; se falso, ritorna `{ ok: false, errore }` senza mai chiamare `salvaCompilazione`. Verificato da 3 test (dato/rifiutato/mancante) + la prova di rottura sopra. 2. **Il consenso si registra sempre, anche negato.** `registraConsenso()` viene chiamata in entrambi i rami di `salvaDalForm` — con `sessione_id = null` quando rifiutato — scrivendo in `profilo_note` (`campo_id = 'consenso_sanitario'`) l'esito letterale e `QUEST_VERSION`. Verificato dal test `registra il consenso anche quando e rifiutato`. 3. **Il codice cliente viene solo dalla sessione.** L'endpoint chiama `codicePerUtente(getIdentityDb(), locals.user!.id)`; il corpo della richiesta non viene mai letto per `client_code`/`clientId`. Verificato dal test che legge il sorgente dell'endpoint e controlla l'assenza letterale di `body.client_code`/`body.clientId`. ## Note per chi esegue il Task 3 - `vista.ts` esiste ed è di proprietà congiunta col Task 3 (che vi aggiunge `refertoDi`, per `progress.md` del piano): non va riscritto da zero. - `src/pages/longevity/questionario.astro` non contiene né `SELECT` né `.prepare(` — rispetta già il vincolo che il test del Task 3 (`nessuna pagina interroga il database da sola`) controlla anche su questo file. - Non ho toccato `src/lib/longevity/anagrafica.ts`: non è servito, la risoluzione utente→codice passa da `codicePerUtente` (già esistente) più le due connessioni singole aggiunte a `db.ts`. --- ## Rinforzo dopo revisione (2026-08-22) **Stato:** completato. Commit `f0629db` sopra a `f5ca841`. **Il problema segnalato:** `src/pages/api/longevity/questionario.ts` apriva ENTRAMBE le connessioni (`getIdentityDb` per risolvere il codice cliente, `getLongevityDb` per salvare), violando la garanzia della spec (§4: un solo modulo tiene insieme identity e longevity). La *logica* di giunzione era già al posto giusto (passava da `codicePerUtente`, che sta in `anagrafica.ts`), ma l'*apertura* delle due connessioni no — e il test che avrebbe dovuto impedirlo cercava due nomi letterali (`createIdentityDb`/`createLongevityDb`) che i nomi nuovi (`getIdentityDb`/`getLongevityDb`, aggiunti da me nello stesso task) non toccavano affatto. ### Correzione 1: l'endpoint apre solo longevity `src/lib/longevity/anagrafica.ts` — aggiunta: ```ts import { getIdentityDb } from './db'; ... export function codicePerUtenteLoggato(userId: number): string | null { return codicePerUtente(getIdentityDb(), userId); } ``` `anagrafica.ts` è il modulo autorizzato: apre lei stessa `identity` (via `getIdentityDb`, già usata altrove nel file in senso lato) e resta l'unico punto che tiene insieme le due connessioni nella stessa funzione chiamante. `src/pages/api/longevity/questionario.ts` — non importa più `getIdentityDb` né `codicePerUtente`; importa solo `getLongevityDb` (per `salvaDalForm`) e `codicePerUtenteLoggato` (per il codice cliente). Verificato che il file non contenga più la stringa `Identity`: ``` grep -n "Identity" src/pages/api/longevity/questionario.ts ``` Output: nessuna riga, `grep` esce con stato 1. Il test del Task 2 (`il codice cliente NON puo arrivare da chi chiama la pagina`, che verifica `/codicePerUtente/` nel sorgente dell'endpoint) resta verde perché `codicePerUtenteLoggato` contiene `codicePerUtente` come sottostringa letterale — non è un trucco per far passare il test, è il nome naturale della funzione wrapper. ### Correzione 2: il test anti-giunzione, reso strutturale `tests/longevity/anagrafica.test.ts` — il test che cercava `src.includes('createIdentityDb') && src.includes('createLongevityDb')` è stato sostituito (non affiancato: il vecchio controllo è un sottoinsieme stretto del nuovo, non catturava nessun caso che il nuovo non catturi) con: ```ts if (src.includes('Identity') && src.includes('Longevity')) { colpevoli.push(f); } ``` Cerca i due riferimenti letterali "Identity"/"Longevity" (con l'iniziale maiuscola, la stessa convenzione con cui compaiono in QUALUNQUE accessore camelCase esportato da `db.ts` — `createIdentityDb`, `getIdentityDb`, un ipotetico `apriIdentityDb` futuro — tutti contengono quella sottostringa) invece di un elenco chiuso di nomi. Non soffre di falsi positivi dal resto del sito: verificato con ``` grep -rln "Identity" src --include='*.ts' --include='*.astro' ``` che PRIMA della correzione 1 restituiva solo `anagrafica.ts`, `db.ts` e l'endpoint (quest'ultimo il colpevole reale), e dopo la correzione 1 restituisce solo i due file autorizzati. "Longevity" (maiuscola) compare in molti altri file del sito (titoli, testi di prodotto), ma mai insieme a "Identity" fuori dai due moduli autorizzati, quindi l'AND non scatta lì. ### La prova richiesta: rompere il test nuovo con i nomi nuovi Creato un file finto sotto `src/`, con i nomi che il vecchio test non vedeva: ```ts // src/lib/longevity/_prova-giunzione.ts import { getIdentityDb, getLongevityDb } from './db'; export function provaGiunzione() { const identity = getIdentityDb(); const longevity = getLongevityDb(); return { identity, longevity }; } ``` Comando: ``` npm test -- tests/longevity/anagrafica.test.ts ``` Output: ``` ❯ tests/longevity/anagrafica.test.ts (7 tests | 1 failed) 19ms × nessun file in src/ (escluso anagrafica.ts e db.ts) fa riferimento sia a identity sia a longevity AssertionError: expected [ 'lib/longevity/_prova-giunzione.ts' ] to deeply equal [] - [] + [ "lib/longevity/_prova-giunzione.ts" ] Test Files 1 failed (1) Tests 1 failed | 6 passed (7) ``` **Rosso, come atteso** — e con i nomi esatti che avrebbero superato indisturbati il vecchio test. Cancellato il file finto, rilanciato: ``` npm test -- tests/longevity/anagrafica.test.ts ``` ``` Test Files 1 passed (1) Tests 7 passed (7) ``` Torna verde. ### Verifiche finali ``` git status --porcelain ``` ``` M src/lib/longevity/anagrafica.ts M src/pages/api/longevity/questionario.ts M tests/longevity/anagrafica.test.ts ``` ``` npm test ``` ``` Test Files 1 failed | 38 passed (39) Tests 1 failed | 321 passed (322) ``` Stesso rosso pre-esistente e fuori perimetro di prima (`tests/modifiche-agosto.test.ts > ... inaugurazione ...`), nessuna regressione: 321 verdi (invariati rispetto a prima di questo rinforzo, il conteggio dei test non cambia — solo il contenuto di uno di essi). ``` npx tsc --noEmit ``` Output: nessuno (pulito). ### Commit ``` git add src/lib/longevity/anagrafica.ts src/pages/api/longevity/questionario.ts \ tests/longevity/anagrafica.test.ts git commit -m "longevity: l'endpoint del questionario apre solo longevity, e il test anti-giunzione diventa strutturale" ``` ``` [feat/longevity f0629db] longevity: l'endpoint del questionario apre solo longevity, e il test anti-giunzione diventa strutturale 3 files changed, 28 insertions(+), 6 deletions(-) ``` `git status` dopo il commit: albero pulito. --- ## Secondo rinforzo dopo revisione (2026-08-22) **Stato:** completato. Commit `8c98075` sopra a `f0629db`. **Il problema dimostrato dal revisore:** il test anti-giunzione (controllo testuale file-per-file, cerca "Identity" e "Longevity" nello stesso file) si aggira senza malizia con un pattern di re-export su tre file: ```ts // a.ts export { getIdentityDb as connA } from './db'; // b.ts export { getLongevityDb as connB } from './db'; // c.ts import { connA } from './a'; import { connB } from './b'; // usa connA() e connB() insieme — la giunzione esiste, ma nessuno dei tre file // contiene singolarmente sia "Identity" sia "Longevity" ``` Un controllo testuale file-per-file non può vedere una giunzione che attraversa più file: non c'è formulazione del test che lo risolva, va reso impossibile invece che sorvegliato. ### Correzione: la connessione a identity non è più raggiungibile da fuori anagrafica.ts `src/lib/longevity/db.ts` — rimossa l'esportazione di `getIdentityDb`. Resta: - `getLongevityDb` (esportata: il database senza nomi, lo può usare chiunque); - `createIdentityDb` (resta esportata: serve ai test per creare database in memoria — lì la giunzione è voluta ed evidente in un file di test, non in `src/`; il problema era il singleton *condiviso*, non la fabbrica). `src/lib/longevity/anagrafica.ts` — `getIdentityDb` torna dentro, ma **non esportata**: ```ts import { createIdentityDb } from './db'; let identitySingleton: Database.Database | null = null; function getIdentityDb(): Database.Database { if (!identitySingleton) identitySingleton = createIdentityDb(); return identitySingleton; } ``` usata solo da `codicePerUtenteLoggato`. Non essendo esportata, non esiste alcun nome da importare per ottenerla da nessun'altra parte del codice — il vincolo non dipende più da un test che lo sorveglia, non c'è proprio un varco. ### Verifica che il bypass del revisore ora sia impossibile (non solo intercettato) Ricreato lo stesso schema a tre file usato dal revisore, dentro `src/`: ```bash cat > src/lib/longevity/_prova-a.ts <<'EOT' export { getIdentityDb as connA } from './db'; EOT cat > src/lib/longevity/_prova-b.ts <<'EOT' export { getLongevityDb as connB } from './db'; EOT cat > src/lib/longevity/_prova-c.ts <<'EOT' import { connA } from './_prova-a'; import { connB } from './_prova-b'; export function usaEntrambe() { return { a: connA(), b: connB() }; } EOT npx tsc --noEmit ``` Output: ``` src/lib/longevity/_prova-a.ts(1,10): error TS2724: '"./db"' has no exported member named 'getIdentityDb'. Did you mean 'createIdentityDb'? ``` **Fallisce già al typecheck**, prima ancora che un test possa dire qualcosa: non esiste nessun nome da riesportare, quindi il pattern del revisore non parte nemmeno. File di prova cancellati subito dopo: ```bash rm src/lib/longevity/_prova-a.ts src/lib/longevity/_prova-b.ts src/lib/longevity/_prova-c.ts npx tsc --noEmit # pulito ``` ### Il test anti-giunzione: dichiarato onestamente come rete secondaria `tests/longevity/anagrafica.test.ts` — titolo e commento riscritti per non promettere più di quanto il test mantiene: ``` it('rete secondaria: nessun file in src/ (escluso anagrafica.ts e db.ts) fa riferimento sia a identity sia a longevity', ...) ``` Il commento dichiara esplicitamente: la garanzia vera è che `getIdentityDb` non è più esportata da `db.ts` (verificato sopra); il test resta come seconda rete — cattura il caso comune (giunzione dentro un solo file) e protegge contro un domani in cui `getIdentityDb` tornasse esportata per errore — non come prova che la giunzione non possa esistere. ### Due correzioni minori dalla stessa revisione **Eccezioni non gestite nell'endpoint.** `salvaDalForm` delega a `salvaCompilazione`, che lancia se una risposta porta un `test_id` sconosciuto al registro (client disallineato, senza bisogno di malizia). Prima l'endpoint non la intercettava e Astro avrebbe risposto con un 500 generico. Ora, sul modello di `src/pages/api/contact.ts` (che avvolge `sendContactEmail` e risponde pulito): ```ts let esito; try { esito = salvaDalForm(getLongevityDb(), { client_code: codice, data: oggi, eta, risposte, consensoSanitario, liberi }); } catch (err) { console.error('Salvataggio questionario fallito:', err); return json(400, { error: 'Impossibile salvare: alcune risposte non sono riconosciute. Ricarica la pagina e riprova.' }); } ``` **Campo testuale reso sempre numerico nel componente.** `Questionario.tsx` renderizzava sempre ``, anche per un'ipotetica domanda `tipo_valore='txt'` nel registro (oggi nessuna delle 20 lo è, ma la promessa della spec è che aggiungerne una resta un dato, non codice). Ora il tipo dichiarato decide l'input (`text` vs `number`), e all'invio i campi testuali non vengono convertiti con `Number(v)` (che produrrebbe `NaN` e sparirebbe in silenzio): restano esclusi esplicitamente, con un commento che spiega perché non sono ancora agganciati al salvataggio (il canale `valore_txt` non è cablato da questo task, per non ridefinire `vista.ts`/`questionario.ts` oltre quanto richiesto). ### Verifiche finali ``` npm test -- tests/longevity/anagrafica.test.ts tests/longevity/questionario-pagina.test.ts ``` ``` Test Files 2 passed (2) Tests 14 passed (14) ``` ``` npm test ``` ``` Test Files 1 failed | 38 passed (39) Tests 1 failed | 321 passed (322) ``` Stesso rosso pre-esistente e fuori perimetro (`tests/modifiche-agosto.test.ts > ... inaugurazione ...`); nessuna regressione, il test anti-giunzione è fra i 321 verdi. ``` npx tsc --noEmit ``` Output: nessuno (pulito) — sia prima sia dopo la prova col bypass a tre file, verificato separatamente. ``` git status --porcelain ``` ``` M src/components/longevity/Questionario.tsx M src/lib/longevity/anagrafica.ts M src/lib/longevity/db.ts M src/pages/api/longevity/questionario.ts M tests/longevity/anagrafica.test.ts ``` ### Commit ``` git add src/lib/longevity/db.ts src/lib/longevity/anagrafica.ts \ src/pages/api/longevity/questionario.ts \ src/components/longevity/Questionario.tsx \ tests/longevity/anagrafica.test.ts git commit -m "longevity: la connessione a identity non e' piu' raggiungibile fuori da anagrafica.ts" ``` ``` [feat/longevity 8c98075] longevity: la connessione a identity non e' piu' raggiungibile fuori da anagrafica.ts 5 files changed, 75 insertions(+), 28 deletions(-) ``` `git status` dopo il commit: albero pulito.