Coda della modifica precedente, e senza questa quella era una regressione sul
telefono. A colonne uguali la piu' stretta deve contenere la parola piu'
lunga; in layout automatico il browser allargava da solo le colonne piene e
stringeva quelle vuote, quindi il problema non si vedeva.
Il conto, misurato sul Montserrat 400 che il sito serve davvero (non
stimato): REFORMER a .7rem con letter-spacing .1em e' 75,4 px, mentre la
colonna a 720px ne offriva 72,7 di spazio utile — 2,7 in meno. La parola
sarebbe uscita dalla cella proprio dove il minimo si vede, cioe' sul
telefono, che quella tabella la fa scorrere in orizzontale apposta.
A 780px la colonna da' 81,3 px, ~6 di margine. In stampa il conto era gia'
buono (corpo .62rem e padding ridotto: 66,8 px in 77,9).
Il conto e' scritto nel CSS accanto alla regola: chi tocca padding, corpo o
spaziatura deve rifarlo.
185 test verdi.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Chiesto da Adriano. La tabella era in layout automatico, quindi il browser
dimensionava ogni colonna sul contenuto: il sabato, che porta la nota «solo
il 12 settembre», veniva largo quasi il doppio, e i giorni senza class si
stringevano. Una griglia oraria con le colonne di misure diverse si legge
male proprio dove serve — nell'incrocio fra un giorno e un'ora.
`table-layout: fixed` piu' una larghezza sulla sola colonna delle ore
(64px): tutto il resto viene diviso in parti uguali fra le sette dei giorni.
E' lo stesso criterio che il PDF applica da sempre — la' le colonne sono
`(larghezzaUtile - larghezzaOra) / GIORNI.length` — quindi adesso le due
versioni dello stesso foglio si somigliano invece di divergere.
La colonna delle ore resta piu' stretta dei giorni, come nel PDF: ci sta
dentro «12.15» e nient'altro.
185 test verdi.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Chiesto da Adriano l'01/09. Tolte le tre celle del venerdi' dal calendario
'settembre-1-18': MOBILITY alle 8.30, REFORMER alle 9.15 e alle 10.00.
Erano tutte di mattina, quindi in quel periodo il venerdi' resta una colonna
completamente vuota. Resta VUOTA e non barrata: togliere le class non vuol
dire che la sala sia chiusa, e nessuno l'ha detto — il dato distingue le due
cose apposta (`chiusi`), e sceglierne una che non ci e' stata comunicata
sarebbe inventare un'informazione.
⚠️ L'edit e' confinato al primo calendario. Le tre righe del venerdi' sono
IDENTICHE, carattere per carattere, in 'settembre-21-in-poi' — dove il
venerdi' deve restare — quindi una sostituzione sul file intero ne avrebbe
prese sei invece di tre. Verificato dopo: dal 21 settembre il venerdi' ha
ancora PILATES 7.45, MOBILITY 8.30, REFORMER 9.15 e 10.00.
Aggiunto un commento nel dato, perche' confrontando le due griglie il buco
sembra una trascrizione monca e il prossimo che passa lo "corregge".
185 test verdi.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nel repo ora c'e' anche Python — lo script che rigenera la locandina, e i
riferimenti dei test longevity — e ogni esecuzione lascia dietro __pycache__.
Comparivano come file non tracciati a ogni git status, senza essere codice.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Caporali «» attorno alla frase — sono le virgolette italiane, e su un corsivo
di quel corpo reggono meglio delle doppie curve.
Corpo da clamp(1.05rem, 2.4vw, 1.3rem) a clamp(1.35rem, 3.4vw, 1.9rem): ~+45%
sia sul minimo che sul massimo, quindi cresce anche su mobile. Interlinea da
1.8 a 1.5, altrimenti a quel corpo le due righe galleggiavano.
Il <br /> che spezza la frase in due versi resta dov'era.
Tre ritocchi chiesti dal cliente: il "Primo piano" sotto "Prova i nostri
servizi" spariva il messaggio piu' che aiutare, il download della locandina
sta meglio a fine lettura che nell'apertura, e il titolo del programma va
a capo dopo "Come si svolge".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Il test dell'inaugurazione cercava la data dentro il titolo, ma i due sono
stati separati in tag distinti il 19 agosto: era rosso da allora, anche in
produzione.
Il nuovo test tiene insieme le tre cose che devono restare d'accordo, perche'
il QR delle locandine affisse non si corregge piu': la rotta servita dal sito,
l'URL cucito nel codice e il PDF che la pagina offre in download.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
La pagina dell'inaugurazione del 19 settembre: il programma completo della
giornata, gli slot di prova del mattino e del pomeriggio, la festa serale.
Il blocco del titolo è il ritaglio della locandina vera, non una sua
riproduzione: i caratteri originali non li abbiamo, e ricostruirli a occhio
è già stato provato senza riuscirci. Il resto della pagina usa la tipografia
del sito e i colori campionati dalla locandina, così volantino e schermo si
riconoscono come la stessa cosa.
La locandina scaricabile è ricalcata in vettoriale dallo stesso JPEG — tre
colori piatti, quindi ogni forma diventa un tracciato — e monta il QR code
definitivo verso questa pagina, un filo più piccolo dell'originale per
lasciargli attorno il margine muto che lo standard richiede.
I testi non passano più dal pannello contenuti: la pagina è in codice.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sei correzioni dalla revisione finale del 22/08, ognuna con test scritto prima:
1. salvaDalForm calcolava mai i punteggi (calcolaSessione/salvaScore) dopo il
salvataggio: un cliente compilava venti domande e vedeva un referto vuoto.
Ora avviene nella stessa transazione del salvataggio.
2. Il corpo della richiesta dell'endpoint questionario è tipato
(CorpoQuestionario): client_code non esiste in quel tipo, non è più
prendibile per errore. Il vecchio test a lista di nomi vietati era vacuo
(cercava body./corpo., il file usa data.) — sostituito con un test
comportamentale end-to-end sull'endpoint vero.
3. Il consenso: aggiunto il test sul ramo affermativo (non coperto, vacuo
quanto il punto 2); la nota di consenso ora è nella stessa transazione dei
dati sanitari; ha una versione propria (CONSENT_VERSION) invece di quella
del questionario.
4. La prima apertura del referto ora spiega invece di sembrare rotta: Fitness
Age con nota quando manca, spiegazione del radar prima del grafico,
copertura sempre visibile, invito a fare i test in sala col trainer,
numeri formattati all'italiana.
5. Aggiunto il link per ricompilare il questionario nella pagina del referto
(prima spariva appena esisteva una compilazione).
6. Il test anti-query ora scandisce tutte le pagine sotto src/pages/longevity/,
non due nomi a mano. Il test anti-giunzione identity/longevity resta con lo
stesso limite di sempre (createIdentityDb è esportata per creaCliente) —
corretto solo il commento perché lo dichiari onestamente.
Report con comandi e output letterali:
.superpowers/sdd/2026-08-22-longevity-interfaccia/review-finale-report.md
Filtrava le regole sul selettore ("solo quelle con lg-score nel nome"): un elenco
di quali regole guardare invecchia appena nasce una classe nuova, ed e' successo
subito con .lg-radar__voce-nome, .lg-radar__intro, .lg-macro__nota (Task 3), mai
controllate perche' non contengono quella stringa. Tolto il filtro sul selettore:
ora si scandisce ogni regola del foglio (che e' interamente prefissato lg- per
costruzione), con un elenco di eccezioni dichiarate (selettore + valore + motivo)
per gli usi legittimi di un colore fuori lista (il #fff del testo sul bottone
pieno d'accento). Verificato rosso con un colore-giudizio iniettato sia su una
classe del radar sia su una del macro-score, verde ripristinato in entrambi i casi.
Il peso mutato prima era l'unico sotto-dominio del suo asse (questionario_lifestyle,
1.00 in Stile di Vita & Sonno): in una media pesata a un solo elemento disponibile
il peso si semplifica sia al numeratore sia al denominatore, quindi il test restava
verde anche se refertoDi avesse ricalcolato invece di leggere gli score congelati.
Sostituita con il peso di un sotto-dominio ASSENTE in un asse a piu' elementi con
copertura parziale non nulla (hrv in Recupero & Sistema Nervoso): li' un ricalcolo
sposta davvero la copertura oltre soglia. Verificato rosso col ricalcolo forzato,
verde ripristinato.
Il revisore ha dimostrato che il test anti-giunzione si aggira senza malizia:
tre file (uno riesporta la connessione a identity, uno quella a longevity, un
terzo le importa da entrambi e le usa insieme) e nessuno dei tre contiene
singolarmente le due parole cercate. Un controllo testuale file-per-file non
puo' vedere una giunzione che attraversa piu' file - quindi la risposta non
e' un test piu' furbo, e' rendere la cosa impossibile invece di sorvegliarla.
- db.ts: rimossa l'esportazione di getIdentityDb. Resta solo getLongevityDb
(il database senza nomi, puo' usarlo chiunque) e createIdentityDb (serve ai
test per aprire database in memoria - li' la giunzione e' voluta ed
evidente, il problema era il singleton condiviso, non la fabbrica).
- anagrafica.ts: getIdentityDb torna dentro, privata (non esportata) e usata
solo da codicePerUtenteLoggato. Verificato che il bypass a tre file del
revisore ora fallisce gia' a tsc --noEmit ('has no exported member named
getIdentityDb'), non solo a un test che lo sorveglia.
- tests/longevity/anagrafica.test.ts: il test anti-giunzione resta come rete
secondaria, con un commento che dichiara onestamente cosa non vede (una
giunzione via re-export su piu' file) invece di promettere una garanzia che
non mantiene.
Due correzioni minori dalla stessa revisione:
- api/longevity/questionario.ts: salvaDalForm (via salvaCompilazione) lancia
se una risposta porta un test_id sconosciuto al registro; ora e' avvolta in
try/catch e risponde 400 pulito invece di un 500 generico, come gia' fa
src/pages/api/contact.ts per l'invio email.
- components/longevity/Questionario.tsx: un campo con tipo_valore='txt' nel
registro ora rende un input testuale (non piu' sempre "number"), e in
invio i campi testuali non vengono convertiti in NaN e scartati in
silenzio - restano fuori esplicitamente, con un commento che dice perche'.
L'endpoint /api/longevity/questionario apriva anche identity (getIdentityDb)
per risolvere il client_code, violando la garanzia della spec (§4): un solo
modulo, anagrafica.ts, tiene insieme le due connessioni. La logica di
giunzione era gia' al posto giusto (chiamava codicePerUtente, che sta in
anagrafica.ts) ma l'APERTURA delle connessioni no.
- anagrafica.ts: nuova codicePerUtenteLoggato(userId), apre lei stessa
identity via getIdentityDb() (e' il modulo autorizzato) e delega a
codicePerUtente. L'endpoint chiama solo questa: apre esclusivamente
longevity, per salvare.
- tests/longevity/anagrafica.test.ts: il test anti-giunzione cercava due nomi
letterali (createIdentityDb/createLongevityDb) e non vedeva getIdentityDb/
getLongevityDb, nati dopo di lui - la stessa lezione del semaforo nel Task 1
(proteggere il nome invece della cosa). Ora e' strutturale: segnala
qualunque file (esclusi anagrafica.ts e db.ts) che contenga sia "Identity"
sia "Longevity", in qualunque forma. Verificato con un file finto sotto
src/ che apriva entrambe con i nomi nuovi: il test diventa rosso, poi
torna verde dopo la cancellazione.
Sostituisce il prototipo del cliente (salvava anche con "No" al consenso
sanitario, perche' controllava solo che il campo non fosse vuoto). Qui il
consenso si verifica PRIMA di scrivere qualunque dato: se non e' affermativo
non si apre nessuna sessione, e la risposta lo dice. Il rifiuto si registra
comunque fra le note di profilo, con esito e versione del questionario: va
dimostrato tanto quanto un assenso.
- src/lib/longevity/vista.ts: campiDelQuestionario (dal registro, non da una
lista nel codice) e salvaDalForm (consenso prima di tutto, poi delega a
salvaCompilazione).
- src/pages/api/longevity/questionario.ts: il client_code arriva SEMPRE da
codicePerUtente sulla sessione, mai dal corpo della richiesta.
- src/lib/longevity/db.ts: getLongevityDb/getIdentityDb, connessioni singole
per processo (come getDb() del sito), cosi' l'endpoint non apre una nuova
connessione a ogni richiesta.
- src/components/longevity/Questionario.tsx + pages/longevity/questionario.astro:
isola React a quattro blocchi (le tre aree di domande + note/consenso), la
pagina non interroga mai il database da sola.
Cast di FakeContext a onRequest via 'unknown' invece che diretto: i
due tipi non si sovrappongono abbastanza per tsc (FakeContext è un
doppio minimale, non un APIContext completo). Il typecheck è il
meccanismo di sicurezza di questo motore: uno rosso in permanenza è un
cancello che nessuno guarda più.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XhLbMQ1q7wHwJSykgXRwQF
- motore-fisici.test.ts confrontava i tiers letti dal file di
riferimento, non le costanti TIERS_* del codice: ventiquattro numeri
senza custode. Ora identifica la tabella dagli argomenti
dell'oracolo ma usa le costanti vere per calcolare, quindi una
costante alterata non combacia più con nessun caso.
- motore-caso-reale.test.ts: aggiunto il caso end-to-end mancante, un
asse valido (Composizione Corporea, via due righe aggiunte al
registro) e sei insufficienti — oggi la suite copriva solo "tutto
insufficiente".
- test-fisici.ts: commento in testa che dichiara le venti curve non
collegate al motore (vocabolario delle curve senza nomi per loro,
applicaCurva senza modo di ricevere età/sesso) — aggancio è lavoro
del piano degli import, non "una modifica ai dati" come la spec
lascia intendere.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XhLbMQ1q7wHwJSykgXRwQF
- eta_alla_data NULL non usa più il ripiego a zero: senza età la
Fitness Age è null, non un'età di forma negativa dichiarata valida.
Gli assi e i macro-score restano calcolabili (non dipendono dall'età).
- applicaCurva valida i parametri richiesti da ciascuna curva prima di
applicarla: un registro con params vuoti/incompleti dà null, mai il
punteggio pieno che dava lerp con estremi indefiniti.
- calcolaSessione esclude le misure fuori_range=1 (§9 della spec): non
entrano nello score finché non esiste la colonna di conferma (§12,
punto aperto).
- leggiScore(db, sessioneId): legge la tabella score ritipata come
Punteggio, con l'ultimo calcolo per tipo+elemento esplicito nella
query (MAX(id)), non l'ordine naturale delle righe — salvaScore non
sovrascrive di proposito (la storia degli score si tiene, discende
dal congelamento della §6).
- documentata la seconda divergenza dall'oracolo, mai scritta finora:
la Fitness Age esclude gli assi insufficienti, l'oracolo li include
comunque. Comportamento giusto, ma cambia il numero (25,0 contro
34,8 sullo stesso profilo parziale).
- nuovo test che prova che i pesi di due model_version diverse restano
separati (pesiDi filtra su model_version, non li fonde).
Ogni fix verificato in TDD (test rosso prima, verde dopo) e ri-rotto a
mano per confermare che discrimina davvero (vedi report).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XhLbMQ1q7wHwJSykgXRwQF
CHECK ((stato='ok' AND valore NOT NULL) OR (stato='insufficiente' AND
valore NULL)): il tipo Punteggio protegge il codice, non la tabella, e
l'interfaccia leggerà 'score' direttamente. Idempotente sui database
nuovi (CREATE TABLE IF NOT EXISTS); un longevity.db già esistente NON
riceve il vincolo, SQLite non lo permette via ALTER TABLE.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XhLbMQ1q7wHwJSykgXRwQF
Task 6 - la prova sul caso reale documentato dal cliente (dati sintetici,
nessuna persona reale). Il test smontava un difetto nel task 5: RisultatoSessione
esponeva fitnessAge come Punteggio invece che number|null (come lo produce
calcolaFitnessAge in cascata.ts), quindi un fitnessAge insufficiente non era mai
`null`. Corretto in motore/index.ts, con fitnessAgeCopertura a fianco per non
perdere l'informazione che salvaScore scrive in `score`.
Il commento del test 'un test disattivato non entra nel calcolo' diceva
che q_attivita fosse l'unica voce del sotto-dominio questionario_lifestyle:
falso, ne ha sei (q_attivita, q_alimentazione, q_sigarette, q_alcol_life,
q_luce, q_schermi). Il motivo vero per cui il test funziona e' che il
fixture ne compila solo una delle sei, quindi disattivarla svuota il
sotto-dominio. Corretto il commento a dire il motivo vero, con nota su
cosa succederebbe se il fixture cambiasse.
Aggiunto anche il test mancante sulla regola della media: q_attivita(3)=65
e q_alimentazione(6)=60, stesso sotto-dominio, verificano che il valore
dell'asse "Stile di Vita & Sonno" sia la loro media (62.5), diversa sia
dalla somma (125) sia da ciascun addendo (65, 60) — nessun test esistente
controllava il VALORE della media, solo lo stato.
Task 5: applicaCurva smista sulla curva dichiarata in registro_test (non
scritta nel codice); calcolaSessione aggrega le misure attive per
sotto-dominio (media quando piu' test contribuiscono allo stesso
sotto-dominio) e usa cascata.ts coi pesi letti da pesi (pesiDi) per i
sette assi, i tre macro-score e la Fitness Age; salvaScore congela il
risultato in score, con valore a null quando lo stato e' insufficiente.
Corretto anche un refuso nel test del brief ('un test disattivato non
entra nel calcolo'): disattivava q_ore_sonno, ma quella voce sta nel
sotto-dominio questionario_sonno (peso 0.20 su Recupero & Sistema
Nervoso, sempre sotto COPERTURA_MINIMA=0.4 finche' hrv/pressione/hrr non
hanno una voce nel registro), quindi l'asse restava insufficiente prima
e dopo e il JSON risultava identico (verificato: il test falliva con le
due stringhe uguali). Corretto usando q_attivita, l'unica voce del
sotto-dominio questionario_lifestyle che pesa 1.00 su Stile di Vita &
Sonno: disattivarla fa cadere la copertura da 1.00 a 0 e lo stato passa
da ok a insufficiente, una differenza vera.
Bug in aggrega(): la soglia veniva confrontata con la copertura gia'
arrotondata a due decimali, quindi 0.396 (< 0.40) passava per 'ok' perche'
arrotondava a 0.40. Confronto ora sulla copertura grezza; l'arrotondamento
resta solo sul valore esposto. Generalizzata arrotonda1 in curve.ts a un
numero qualsiasi di decimali (bit-exact, banker's rounding) e riusata qui a
due decimali, invece di tenere una seconda logica di arrotondamento nel
motore.
Il test controllava solo l'assenza dell'agilita (scoreAgilita/scoreAgilityMs)
e non della seconda funzione esclusa dal brief, score_generic_range_local:
se qualcuno la reintroducesse un domani nessun test se ne accorgerebbe.
Aggiunta l'assertion con lo stesso criterio, rinominato il test perche' ora
copre entrambe le esclusioni.
Sposta arrotonda1 (round-half-to-even come Python) da test-fisici.ts a
curve.ts, esportata, cosi' entrambi i moduli condividono la stessa funzione
invece di due copie che sui pareggi potrebbero divergere.
Nel farlo, la versione precedente (moltiplica per 10, tolleranza 1e-9 sul
pareggio) si e' rivelata sbagliata su un caso reale: 86.35 non e' un
pareggio nel double che lo rappresenta (vale 86.34999999999999431...,
Python arrotonda a 86.3 senza ambiguita'), ma 86.35*10 arrotonda ESATTAMENTE
a 863.5 in virgola mobile, un pareggio fasullo creato dalla moltiplicazione.
La versione naive dava 86.4. Sostituita con un confronto esatto: scompone il
double in mantissa/esponente (bit IEEE 754) e confronta con aritmetica
razionale su BigInt, senza mai passare per una moltiplicazione che possa
introdurre o cancellare un pareggio.
Aggiunti in motore-curve.test.ts i casi che dimostrano il pareggio vero
(86.25 -> 86.2, 86.75 -> 86.8) e quello fasullo (86.35 -> 86.3, non 86.4:
blocca la regressione appena descritta).
- creaCliente: il codice si deduce dal massimo fra identity E longevity (non piu' solo
identity), cosi' un client_code cancellato dall'anagrafica non torna mai disponibile
e non si attribuiscono le misure di un vecchio cliente a uno nuovo. Compensazione se
la scrittura su longevity fallisce dopo quella su identity.
- ROLES unica fonte in auth.ts (Role e isRole derivati); users.astro e users/new.astro
usano quella lista invece di array scritti a mano che dimenticavano cliente/trainer.
- Commento falso su cosa protegge il middleware, riscritto: rimanda a isProtectedPath.
- vitest.config.ts: alias per astro:middleware (stesso bersaglio della pipeline vite di
Astro), cosi' src/middleware.ts e' finalmente importabile e testabile in isolamento -
prima nessun test lo caricava davvero.
- export.test.ts: asserzione posizionale per colonna, non piu' solo intestazioni scritte
a mano; intercetta un riordino della SELECT che disallinea etichette e valori.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>