Files
Adriano b913a2bf90 registri dei piani in git (AI-OS#309)
Refs gitea:Adriano/AI-OS#310

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 10:36:26 +02:00

540 lines
22 KiB
Markdown
Raw Permalink 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.
# 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 `<input type="number">`, 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.