Files
InsanityLab-web/.superpowers/sdd/2026-08-22-longevity-interfaccia/task-2-report.md
T
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

22 KiB
Raw Blame History

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:

 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:

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:

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:

// 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:

// 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:

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/:

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:

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):

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.