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>
This commit is contained in:
2026-10-06 10:36:26 +02:00
parent 9126af28ce
commit b913a2bf90
94 changed files with 40294 additions and 0 deletions
@@ -0,0 +1,539 @@
# 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.