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

25 KiB

IN-SANITY LONGEVITY PROJECT — piattaforma dati cliente

Data: 2026-08-21 Stato: design approvato, implementazione non iniziata Branch: feat/longevitymain 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_codeuser_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

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

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 assiRecupero 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