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
This commit is contained in:
@@ -0,0 +1,504 @@
|
||||
# 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 |
|
||||
Reference in New Issue
Block a user