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
25 KiB
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
-- ============ 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:
- Gli score si salvano, non si ricalcolano a ogni visualizzazione. La
tabella
scoreè la memoria di ciò che il cliente ha visto. - 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.
- Base giuridica per i dati sanitari (art. 9 GDPR). Il campo
consensodel 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. - Chi è il medico supervisore, nominato nella specifica del questionario. Se è un professionista sanitario, cambia la base giuridica utilizzabile (art. 9.2.h), in meglio.
- 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.
- Il cambio di versione sul grafico della progressione (§6).
- Il registro delle versioni: cosa cambia a ogni incremento di
quest_versionemodel_version, altrimenti fra sei mesi «v1.3» non dirà niente a nessuno. - 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.
- 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
- 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.
- 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.
- 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.
registraMisurevalida l'esistenza del test, non il valore. Oggi accetta una misura senza nessun valore, un testo dove il registro dichiara un numero, e unNaN(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%.seedRegistrorilanciato disfa il registro. UsaINSERT OR REPLACEe riazzeraattivo_ae le note: disattivare un test e poi rilanciare il seed lo riattiva. È esattamente l'opposto della promessa della §5, dove disattivare un test è valorizzareattivo_a.MODEL_VERSIONnon esiste.QUEST_VERSIONsta inquestionario.ts; la gemella che versiona curve e pesi non è dichiarata da nessuna parte, eseedPesila riceve come parametro che solo i test valorizzano. Finché non esiste, il congelamento del §6 ha metà del suo significato.- La soglia del 40% è una costante di codice, non un parametro nel registro come chiede la §11.3 — e nessuno la consuma ancora.
- 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 |