Files
InsanityLab-web/apps/sito/docs/specs/2026-08-21-longevity-design.md
AdrianoDev 510d7ca4ec Struttura: il sito esistente si sposta sotto apps/sito
Primo passo della separazione sito / piattaforme decisa da Adriano il 03/09:
due applicazioni autosufficienti, senza workspace di root, cosi' che ognuna
resti un progetto Astro completo ed estraibile.

Qui non cambia niente del codice: solo la radice. I pattern del .gitignore
che avevano una barra erano ancorati alla radice e dopo lo spostamento non
avrebbero piu' coperto niente (data/*.db*, uploads/*): resi validi a
qualunque profondita'.

Suite: 45 file, 347 test verdi prima e dopo lo spostamento.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EndnceRA5WnA6rvA5iV9WL
2026-09-03 11:05:54 +00:00

505 lines
25 KiB
Markdown

# IN-SANITY LONGEVITY PROJECT — piattaforma dati cliente
**Data:** 2026-08-21
**Stato:** design approvato, implementazione non iniziata
**Branch:** `feat/longevity``main` non viene toccato, nessun deploy
**Committente:** In-Sanity Lab (Nicola Antonelli) — sviluppo Tielogic
---
## 1. Cos'è, e come si chiama
Il nome esiste già e viene dal cliente: **IN-SANITY LONGEVITY PROJECT** è
l'iniziativa — *«servizio premium basato su dati, con monitoraggio
longitudinale»* — e **IN-SANITY LONGEVITY SCORE** è il sistema di punteggio che
ne è il prodotto di punta. Questa piattaforma è l'implementazione software del
primo. Nel codice il nome tecnico è **`longevity`**.
Non è una raccolta di applicazioni separate: è **un fascicolo del cliente**.
Questionario, check-up in sala, Wellness Tower e VALD non sono quattro
sottosistemi, sono **quattro sorgenti che scrivono nello stesso posto**;
dashboard, vista trainer e punteggi sono **viste e funzioni derivate** di quel
posto. È la differenza fra progettare per dati e progettare per schermate, e
determina tutto quello che segue.
## 2. Perimetro
**Dentro:** anagrafica cliente, questionario in pagina, registrazione delle
misure da tutte le sorgenti, motore di calcolo degli score, dashboard cliente,
vista trainer, export per le statistiche.
**Fuori, e per ragioni diverse:**
- **I percentili interni ISL** — non per scelta: la documentazione del cliente
dichiara che sotto 15 campioni sono inutilizzabili e sotto 30 provvisori.
Vanno costruiti quando i dati esistono, non prima.
- **Gli 8 indicatori posturali** della Wellness Tower — esistono solo nel PDF
stampato, non nell'export a 55 colonne. Il cliente li ha già esclusi dallo
score.
- **Il blocco Trimestre / 2 Mesocicli** — dichiarato aperto dal cliente stesso e
ancora in analisi da parte sua.
- **Il deploy** — la piattaforma si sviluppa e si prova in locale. Quando mettere
qualcosa in produzione è una decisione separata, che non appartiene a questo
documento.
## 3. Collocazione
La piattaforma vive **dentro l'applicazione Astro esistente**, come già fanno
Biohacking Campus e Stress Index. Non è un secondo servizio, non è un secondo
container.
```
src/pages/longevity/ rotte visibili
src/pages/api/longevity/ endpoint
src/lib/longevity/ modello dati, motore, import
tests/longevity/ test
```
Il motivo per cui non è un progetto separato è che l'autenticazione, le sessioni
e l'anagrafica utenti esistono già qui e funzionano in produzione: rifarle altrove
significherebbe mantenerne due.
⚠️ **Il calcolo è in TypeScript, non in Python.** Il motore fornito dal cliente
(`isl_scoring_engine.py`, 537 righe) è aritmetica pura — interpolazioni lineari,
tabelle di soglie, medie pesate — e importa solo `dataclasses` e `typing`: nessuna
libreria numerica. Portarlo in TypeScript costa poco e evita un secondo runtime
dentro un'immagine `node:22-slim` a processo unico. La stessa scelta vale per il
prototipo del questionario, che calcola già in JavaScript: tenere le formule in
due linguaggi è il modo silenzioso di farle divergere.
## 4. I tre database
SQLite, tre file distinti, aperti da connessioni distinte.
| File | Contiene | Non contiene |
|---|---|---|
| `insanitylab.db` | il sito: articoli, contenuti, **utenti e sessioni** | niente di clinico |
| `identity.db` | `client_code``user_id`, nome, cognome, nascita, recapiti | nessuna misura |
| `longevity.db` | misure, questionari, punteggi, registro test | **nessun nome, mai** |
La proprietà che ne discende è la richiesta esplicita del cliente del 21/08: *«la
mappatura codice↔nome va tenuta in un posto separato e più ristretto rispetto
all'export usato per le statistiche, così anche chi ha accesso ai dati clinici
non può risalire all'identità»*. Qui l'export per le statistiche **è**
`longevity.db`: già pseudonimizzato per costruzione, senza una funzione di
anonimizzazione che qualcuno debba ricordarsi di chiamare.
**Vincolo di implementazione, verificabile con un test:** esiste **un solo
modulo** che apre insieme `identity` e `longevity`. Nessun altro punto del codice
importa entrambe le connessioni.
## 5. Modello dati
```sql
-- ============ identity.db ============
CREATE TABLE clienti (
client_code TEXT PRIMARY KEY, -- es. 'ISL-0007'
user_id INTEGER, -- users.id in insanitylab.db (nessuna FK: file diverso)
nome TEXT NOT NULL,
cognome TEXT NOT NULL,
data_nascita TEXT, -- ISO; resta QUI, non passa in longevity
email TEXT,
telefono TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE UNIQUE INDEX idx_clienti_user ON clienti (user_id);
-- ============ longevity.db ============
CREATE TABLE soggetti (
client_code TEXT PRIMARY KEY,
sesso TEXT NOT NULL CHECK (sesso IN ('M','F')),
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE sessioni (
id INTEGER PRIMARY KEY AUTOINCREMENT,
client_code TEXT NOT NULL REFERENCES soggetti(client_code),
data TEXT NOT NULL,
tipo TEXT NOT NULL CHECK (tipo IN ('checkup','questionario')),
eta_alla_data INTEGER, -- l'età serve al motore; la data di nascita no
operatore TEXT,
note TEXT,
quest_version TEXT, -- solo per tipo='questionario'
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_sessioni_cliente ON sessioni (client_code, data DESC);
CREATE TABLE misure (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sessione_id INTEGER NOT NULL REFERENCES sessioni(id) ON DELETE CASCADE,
client_code TEXT NOT NULL REFERENCES soggetti(client_code),
test_id TEXT NOT NULL REFERENCES registro_test(test_id),
valore_num REAL,
valore_txt TEXT, -- per le risposte non numeriche
unita TEXT,
fonte TEXT NOT NULL CHECK (fonte IN
('manuale','questionario','wellness_tower','vald','calibre','stress_index')),
fuori_range INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_misure_cliente_test ON misure (client_code, test_id);
CREATE INDEX idx_misure_sessione ON misure (sessione_id);
CREATE TABLE registro_test (
test_id TEXT PRIMARY KEY, -- 'handgrip_dx', 'plank', 'q_sonno_ore'
etichetta TEXT NOT NULL,
unita TEXT,
tipo_valore TEXT NOT NULL CHECK (tipo_valore IN ('num','txt')),
curva TEXT, -- 'bell','lin_dec','inc_plateau','x10','x10_inv','decstep','incstep'
params TEXT, -- JSON dei parametri della curva
asse TEXT, -- uno dei 7 assi; NULL = non entra nello score
sotto_dominio TEXT, -- a QUALE sotto-dominio contribuisce; il peso sta in `pesi`
range_min REAL, -- atteso, non vincolante: fuori range si marca
range_max REAL,
attivo_da TEXT NOT NULL,
attivo_a TEXT, -- NULL = ancora attivo
note TEXT
);
CREATE TABLE pesi (
model_version TEXT NOT NULL,
livello TEXT NOT NULL CHECK (livello IN ('asse','macro','fitness_age')),
contenitore TEXT NOT NULL, -- l'asse, il macro-score, o 'fitness_age'
elemento TEXT NOT NULL, -- il sotto-dominio, o l'asse, o la voce isolata
peso REAL NOT NULL,
PRIMARY KEY (model_version, livello, contenitore, elemento)
);
CREATE TABLE profilo_note (
id INTEGER PRIMARY KEY AUTOINCREMENT,
client_code TEXT NOT NULL REFERENCES soggetti(client_code),
sessione_id INTEGER REFERENCES sessioni(id),
campo_id TEXT NOT NULL, -- 'farmaci', 'problematiche_attuali', 'obiettivi'
testo TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE score (
id INTEGER PRIMARY KEY AUTOINCREMENT,
client_code TEXT NOT NULL REFERENCES soggetti(client_code),
sessione_id INTEGER NOT NULL REFERENCES sessioni(id),
tipo TEXT NOT NULL CHECK (tipo IN ('asse','macro','fitness_age')),
elemento TEXT NOT NULL, -- 'Forza & Struttura', 'PERFORMANCE', ...
valore REAL, -- NULL quando stato='insufficiente'
copertura REAL NOT NULL,
stato TEXT NOT NULL CHECK (stato IN ('ok','insufficiente')),
quest_version TEXT,
model_version TEXT NOT NULL,
calcolato_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_score_cliente ON score (client_code, calcolato_at DESC);
```
### Perché l'età e non la data di nascita
Il motore ha bisogno dell'età — `score_handgrip(kg, eta, sex)`,
`score_vo2max(vo2max, eta, sex)`, le bande per decade — ma gli serve **un numero
al momento della misura**, non una data. Sesso ed età non identificano nessuno;
una data di nascita esatta, combinata con pochi altri campi, quasi sì. Costa una
colonna e toglie un quasi-identificatore dal database clinico.
### Perché il peso non sta sulla riga del test
Un sotto-dominio può essere coperto da **test alternativi**: la *spinta* si
misura con i push-up o con il 5RM di panca, a seconda del livello del cliente, e
il peso — 20% dentro *Forza & Struttura* — è del sotto-dominio, non del singolo
test. Se il peso stesse sulla riga del test si ripeterebbe su ogni alternativa,
e prima o poi due copie divergerebbero.
I pesi stanno quindi in una tabella loro, **con la `model_version` nella chiave**.
È ciò che rende reale il congelamento del §6: uno score del passato si può
rileggere con i pesi che erano in vigore quando è stato calcolato, invece che con
quelli di oggi. Senza questo, `model_version` sarebbe un'etichetta senza niente
dietro.
⚠️ Nota per l'implementazione: nel motore Python `plank` compare in **due** assi
`core` in *Forza & Struttura* (0.15) e `plank` in *Stabilità & Mobilità* (0.20).
È lo stesso doppio conteggio ammesso dal cliente per l'handgrip nella Fitness Age,
ma quello del plank **non è dichiarato da nessuna parte**. Questo schema lo
rappresenta senza problemi (due righe in `pesi`), però va segnalato al cliente
invece che riprodotto in silenzio.
### Perché il registro dei test è dato e non codice
Il cliente ha già i test in forma di registro: un foglio di 30 righe con fonte e
stato per ciascuno. Qui diventa una tabella, e la conseguenza è che **il set dei
test è mobile senza migrazioni**, cosa che è già successa tre volte in un mese:
l'agilità è stata rimossa dallo score, la capacità vitale pure, e l'HRR è passato
dalla finestra a 2' a quella a 1'.
Con questo modello: disattivare un test è valorizzare `attivo_a`; cambiare la
finestra dell'HRR è **un `test_id` nuovo che convive col vecchio**, e le
misurazioni già fatte restano leggibili e restano marcate per quello che sono,
invece di diventare confrontabili per sbaglio con le nuove.
## 6. Versioni e congelamento
Decisione del cliente, 21/08: quando cambia una formula i punteggi storici si
**congelano**, non si ricalcolano. La ragione è di prodotto ed è corretta — se
lo score storico cambia, il cliente non distingue più il proprio miglioramento
da un cambio di matematica, e il confronto nel tempo perde valore.
L'implementazione ha due conseguenze:
1. **Gli score si salvano**, non si ricalcolano a ogni visualizzazione. La
tabella `score` è la memoria di ciò che il cliente ha visto.
2. **Le misure grezze si salvano tutte**, quindi un ricalcolo con formule nuove
resta sempre possibile in lettura, per analisi, senza sovrascrivere niente.
**Due versioni, non una.** Ogni score porta `quest_version` (quali domande) e
`model_version` (quali curve e quali pesi). Il cliente ne ha implementata una
sola perché dal suo lato coincidevano, ma i pesi degli assi possono cambiare
senza che cambi una domanda: con una versione sola un punteggio del passato non
saprebbe dire quale delle due cose è cambiata.
⚠️ **Il congelamento sposta il problema del confronto, non lo elimina.** Due
compilazioni con versioni diverse restano non confrontabili anche se nessuno le
ha riscritte. Il grafico della progressione deve quindi **dichiarare
visivamente il cambio di versione**, altrimenti il cliente vede una linea
continua di numeri calcolati con matematiche diverse — che è l'equivoco che la
decisione voleva evitare.
## 7. Flusso dei dati
### Ingresso: un solo varco
```
questionario in pagina ─┐
gestionale trainer ─┤
import file (WT, VALD) ─┼─→ registraMisure(sessione, misure[]) ─→ misure
API (VALD Hub) ─┘ ↑ valida contro registro_test
```
`registraMisure` è l'unica funzione che scrive in `misure`. Valida ogni riga
contro il registro: **un `test_id` sconosciuto viene rifiutato**, non scritto in
silenzio. Un valore fuori dal range atteso viene invece **scritto e marcato**
(`fuori_range = 1`), perché scartarlo perderebbe un dato vero e clamparlo lo
falserebbe — vedi §9.
### Calcolo
Il motore legge misure e registro, e produce gli score seguendo la cascata a
quattro livelli del cliente: sotto-metrica → asse → macro-score → Fitness Age,
con rinormalizzazione dei pesi disponibili a ogni livello e soglia di copertura
al **40%**.
Il tipo di ritorno impedisce la trappola presente nel motore Python, dove
`aggregate()` restituisce il punteggio pieno **anche quando lo dichiara
insufficiente**:
```ts
type Punteggio =
| { stato: 'ok'; valore: number; copertura: number }
| { stato: 'insufficiente'; copertura: number } // non c'è nessun valore da leggere
```
La regola di prodotto — *«tratteggiato, mai un numero pieno fasullo»* — diventa
così impossibile da violare per distrazione: chi disegna il radar non ha il campo
da cui prendere il numero.
### Uscita
Dashboard cliente (radar a 7 assi, 3 macro-score, Fitness Age), vista trainer, ed
export per le statistiche — che è il contenuto di `longevity.db`, senza passare
da nessuna funzione di anonimizzazione.
## 8. Ruoli e accessi
Il sito ha oggi `admin | superuser | user | piattaforme`, e nessuno di questi è
il cliente finale né il trainer. Se ne aggiungono **due**: `cliente` e `trainer`.
| Rotta | Chi entra |
|---|---|
| `/longevity/io` | `cliente` (solo il proprio fascicolo), `trainer`, `admin` |
| `/longevity/gestionale` | `trainer`, `admin` |
| `/api/longevity/**` | come sopra, verificato lato server |
**Il cliente vede solo i propri dati, e non è una regola di interfaccia:** gli
endpoint ricavano il `client_code` dalla sessione e **mai** dalla richiesta. Un
client_code che arriva dal browser viene ignorato.
## 9. Errori e casi limite
**Misura fuori dal range atteso: si registra e si marca.** Il caso è già
successo — la capacità vitale di 5148 mL su un fondoscala dichiarato di
2000-3000 mL non era una prestazione eccezionale, era il range del fornitore
sbagliato, e clamparla a 100 produceva un dato apparentemente ottimo. Le misure
marcate finiscono in una lista che il trainer vede; non entrano nello score
finché qualcuno non le conferma.
**Scala non comparabile: si esclude dal registro, non si corregge nel codice.**
L'agilità a 548 ms contro i 250 attesi dava 20/100 su Stabilità — falso, e la
causa era il protocollo diverso. Un caso così si risolve disattivando il test nel
registro, non aggiungendo un fattore di correzione da qualche parte.
**Copertura sotto il 40%:** l'elemento è `insufficiente` e non ha un valore.
**Test sconosciuto in ingresso:** errore esplicito, la sessione non viene scritta
a metà.
## 10. Test
Il test principale è il **verificatore contro il motore Python**: gli stessi
ingressi devono produrre gli stessi numeri, e ogni curva va confrontata su tutto
il suo dominio, non su un caso singolo. È l'unico modo per sapere che il porting
non ha spostato niente.
Poi: le regole di copertura e rinormalizzazione, il rifiuto dei test sconosciuti,
la marcatura del fuori range, l'isolamento dei tre database (nessun modulo, a
parte quello designato, apre insieme identity e longevity), e il fatto che un
`client_code` proveniente dalla richiesta non venga mai onorato.
⚠️ **I dati reali di Donata e Nicola non entrano nelle fixture.** Sono dati
sanitari di due persone identificabili, e le fixture stanno in git, dove restano
per sempre e le legge chiunque abbia accesso al repository. I casi di prova si
costruiscono sintetici; i referti veri restano dove sono, per il confronto a mano
quando serve.
## 11. Punti aperti che il codice non risolve
Questi non sono dettagli implementativi: sono decisioni che spettano al cliente,
e finché non arrivano il codice si comporterà come qui dichiarato — non come
capita.
1. **Base giuridica per i dati sanitari (art. 9 GDPR).** Il campo `consenso` del
questionario riguarda le comunicazioni promozionali, non il trattamento di
dati di salute, e la finalità statistica è ulteriore rispetto a quella di
cura. Serve un consenso esplicito dedicato. **Non è un blocco allo sviluppo,
è un blocco alla messa in produzione con clienti veri.**
2. **Chi è il medico supervisore**, nominato nella specifica del questionario. Se
è un professionista sanitario, cambia la base giuridica utilizzabile
(art. 9.2.h), in meglio.
3. **La regola dei buchi nel questionario.** Oggi esiste in due versioni: il
prototipo annulla un sotto-dominio solo se non c'è **nessuna** risposta, il
motore usa il 40%. La piattaforma userà **una soglia unica, dichiarata nel
registro come parametro**, e in attesa della risposta assume il **40%**,
coerente con la regola dei sette assi.
4. **Il cambio di versione sul grafico** della progressione (§6).
5. **Il registro delle versioni**: cosa cambia a ogni incremento di
`quest_version` e `model_version`, altrimenti fra sei mesi «v1.3» non dirà
niente a nessuno.
6. **Nomi dei Livelli 1/2/3** — SPARK/FLUX/APEX o PULSE/FLOW/PEAK, aperti da
sessioni precedenti del cliente. Il livello è già una colonna del profilo.
7. **Export CSV/API di Stress Index**, l'unico canale la cui automazione non è
confermata. L'HRV entra nel motore come passthrough, quindi senza quel canale
va inserito a mano.
## 12-bis. Cosa ha scoperto la costruzione del motore (22/08)
Il motore è stato costruito il 22/08 (ramo `feat/longevity`, 6 task, 308 test). Il porting
è fedele: eseguendo la funzione dimostrativa del cliente sui due motori, i sette assi, i tre
macro-score e la Fitness Age **combaciano numero per numero**.
### Due divergenze deliberate dall'oracolo, entrambe volute
1. **Un punteggio insufficiente non espone un valore.** Nell'originale la funzione di
aggregazione restituisce il numero anche quando dichiara l'elemento insufficiente; qui il
tipo non ha proprio il campo. È ciò che rende impossibile mostrare un numero dove i dati
non bastano.
2. **La Fitness Age esclude gli assi insufficienti**, mentre l'oracolo li include comunque.
⚠️ **Cambia il numero, e parecchio**: sullo stesso profilo parziale, 25,0 contro 34,8 —
quasi dieci anni. Chi confrontasse il nostro referto con il Python del cliente su un
profilo incompleto concluderebbe che il porting è rotto. **Da dire a Nicola.**
### ⚠️ Le curve dei test fisici non sono collegate, e agganciarle non sarà «solo dati»
Le venti curve dei test misurati sono portate e verificate, ma **irraggiungibili dal
motore**: il vocabolario delle curve del registro non ha nomi per loro, e la funzione che
applica una curva non può ricevere età e sesso, che quasi tutte richiedono.
📌 Questo contraddice la §5, dove il registro è presentato come dato che si estende senza
toccare il codice. **È vero per il questionario, non per i test fisici**: agganciarli
richiede di estendere il vocabolario e la firma. Il piano degli import deve saperlo, perché
oggi crede di trovare la presa già montata.
### Il confine dove la barriera finisce
Il tipo protegge il codice; la tabella `score` ha ora anche un vincolo che rifiuta un valore
scritto sotto uno stato che lo nega. ⚠️ Ma quel vincolo entra solo nei database **creati da
qui in avanti**: lo schema gira con `CREATE TABLE IF NOT EXISTS`, che su un file esistente
non fa nulla. Oggi nessun database esiste ancora — diventa un problema il giorno del primo
deploy, ed è quello il momento di ricordarsene.
### La storia dei punteggi si tiene
Un ricalcolo **aggiunge** righe invece di sovrascriverle, coerentemente col congelamento del
§6: sovrascrivere cancellerebbe proprio ciò che il congelamento protegge. La lettura passa da
una funzione tipata che restituisce l'ultimo calcolo per sessione — la dashboard non deve
inventarsi una query sua.
### 🔲 Una domanda per Nicola, sulla voce più pesante del modello
`hrv` pesa **0,50 su due assi***Recupero* ed *Energia* — ed è la singola voce più pesante
di tutto il modello. Nel motore è un dato solo, condiviso. Ma nella funzione dimostrativa
dell'oracolo i due assi ricevono **numeri diversi sotto lo stesso nome** (68 e 65).
O è la stessa misura riusata su due assi — come il plank, che Nicola ha già confermato — e i
due numeri sono illustrativi; oppure sono due metriche distinte, e allora le stiamo fondendo
per sbaglio. Diventa concreto appena si collega Stress Index, che è la sorgente di quel dato.
---
## 12. Cosa l'implementazione ha scoperto — da chiudere prima del motore
Lo strato dati è stato costruito il 21/08 (ramo `feat/longevity`, 19 commit, 241 test).
Costruirlo ha fatto emergere sei punti che questo documento non aveva previsto o su cui
si contraddiceva. **Nessuno è bloccante per lo strato dati; tutti lo diventano per il
motore di calcolo.**
1. **La conferma delle misure fuori range non ha dove stare.** La §9 dice che una misura
marcata «non entra nello score finché qualcuno non la conferma», ma lo schema della §5
non prevede nessuna colonna che registri quella conferma. Le due sezioni si
contraddicono: va aggiunta la colonna o riscritta la regola.
2. **`registraMisure` valida l'esistenza del test, non il valore.** Oggi accetta una
misura senza nessun valore, un testo dove il registro dichiara un numero, e un `NaN`
(che viene scritto come nullo e **non** marcato fuori range, perché ogni confronto con
NaN è falso). Una riga vuota conta come «test presente» ai fini della copertura del 40%.
3. **`seedRegistro` rilanciato disfa il registro.** Usa `INSERT OR REPLACE` e riazzera
`attivo_a` e le note: disattivare un test e poi rilanciare il seed lo riattiva. È
esattamente l'opposto della promessa della §5, dove disattivare un test è valorizzare
`attivo_a`.
4. **`MODEL_VERSION` non esiste.** `QUEST_VERSION` sta in `questionario.ts`; la gemella
che versiona curve e pesi non è dichiarata da nessuna parte, e `seedPesi` la riceve
come parametro che solo i test valorizzano. Finché non esiste, il congelamento del §6
ha metà del suo significato.
5. **La soglia del 40% è una costante di codice**, non un parametro nel registro come
chiede la §11.3 — e nessuno la consuma ancora.
6. **Registro e pesi sono disallineati per costruzione.** Il registro contiene oggi i 20
campi del questionario; i pesi citano handgrip, vo2max, plank, flamingo — sotto-domini
che nessun test ancora alimenta. Con i dati attuali sei assi su sette resterebbero
permanentemente «insufficiente». È atteso, perché i test fisici arrivano col piano degli
import, ma va saputo: il primo che lancia il motore lo legge come un difetto.
⚠️ **Una trappola già armata per il piano delle API.** Nelle regole di accesso, tutto ciò
che sta sotto `/api/longevity/` e non sotto `/api/longevity/gestionale/` è raggiungibile da
**ogni** cliente. Oggi è innocuo perché non esiste nessun endpoint, ma significa che la
scelta dei nomi delle rotte è una questione di sicurezza, non di stile: un domani
`/api/longevity/clienti` sarebbe l'elenco di tutti, aperto a tutti.
⚠️ **Due limiti noti dell'anagrafica**, accettati con cognizione: se fallisse anche la
cancellazione compensativa su identity, l'errore diagnostico verrebbe mascherato e
resterebbe un cliente senza pendant clinico; e i codici sono ordinati come stringhe,
quindi oltre `ISL-9999` l'ordinamento sbaglia e un codice potrebbe essere riusato.
📌 **Da dire a Nicola, non ancora detto:** il plank pesa su due assi — `core` in *Forza &
Struttura* (0.15) e `plank` in *Stabilità & Mobilità* (0.20). È lo stesso doppio conteggio
che lui dichiara per l'handgrip nella Fitness Age, ma questo non è dichiarato da nessuna
parte.
---
## 12. Decisioni prese, e da chi
| Decisione | Chi | Quando |
|---|---|---|
| Un record per compilazione + export Excel | Nicola | 21/08 |
| Versione del questionario su ogni record | Nicola | 21/08 |
| Formula cambiata → punteggi storici congelati | Nicola | 21/08 |
| Export pseudonimizzato, mappatura separata e ristretta | Nicola | 21/08 |
| Curva alcol 0-7 = 100 (soglia NIAAA/WHO), voluta | Nicola | 21/08 |
| La piattaforma vive dentro il sito, area riservata | Adriano | 13/08 |
| Ramo separato, nessun deploy | Adriano | 21/08 |
| Tre database separati | Adriano | 21/08 |
| Registro di misure invece di colonne fisse | Adriano | 21/08 |
| Il progetto è unico: un framework sui dati del cliente | Adriano | 21/08 |
| Calcolo in TypeScript, non Python | Tielogic (verificato sul codice) | 21/08 |