longevity: test fisici nel registro e nel calcolo (prerequisito del gestionale) #1

Closed
opened 2026-09-26 12:22:12 +00:00 by Adriano · 4 comments
Owner

COSA: il registro contiene solo le 20 domande del questionario; le 20 curve fisiche di apps/platforms/src/lib/longevity/motore/test-fisici.ts sono verificate contro l'oracolo ma non collegate a calcolaSessione (motore/index.ts). Senza questo, 6 assi su 7 restano 'insufficiente' qualunque cosa inserisca il trainer.
PERCHÉ: il gestionale trainer (issue seguente) inserisce misure fisiche; devono entrare nel punteggio.
FATTO QUANDO: registro con le misure grezze fisiche; sotto-domini fisici calcolati con età e sesso; la sessione di check-up e quella di questionario si combinano nel punteggio; caso demo dell'oracolo riprodotto in un test; test verdi.

COSA: il registro contiene solo le 20 domande del questionario; le 20 curve fisiche di apps/platforms/src/lib/longevity/motore/test-fisici.ts sono verificate contro l'oracolo ma non collegate a calcolaSessione (motore/index.ts). Senza questo, 6 assi su 7 restano 'insufficiente' qualunque cosa inserisca il trainer. PERCHÉ: il gestionale trainer (issue seguente) inserisce misure fisiche; devono entrare nel punteggio. FATTO QUANDO: registro con le misure grezze fisiche; sotto-domini fisici calcolati con età e sesso; la sessione di check-up e quella di questionario si combinano nel punteggio; caso demo dell'oracolo riprodotto in un test; test verdi.
Adriano added the sviluppo label 2026-09-26 12:22:12 +00:00
Adriano added the assegnata:goldrake label 2026-09-26 12:23:39 +00:00
Author
Owner

🤖 goldrake — preso in carico 2026-09-26T12:23:39Z
ramo: lavoro/goldrake · capacità: docker, pubblica, sempre-accesa, systemd

🤖 goldrake — **preso in carico** 2026-09-26T12:23:39Z ramo: `lavoro/goldrake` · capacità: docker, pubblica, sempre-accesa, systemd
Author
Owner

ESITO: richiesta
RIF: gitea:Adriano/InsanityLab-web#1
COSA: analisi (passo 1). Misure fisiche grezze nel registro, sotto-domini fisici calcolati con età e sesso, sessioni combinate nel punteggio.
RAMO: lavoro/goldrake — clone di lavoro goldrake:~/lavoro/InsanityLab-web (NON /opt/docker/insanitylab/src, che è la produzione)
BASE: 13d8cdd, apps/platforms: 27 file di test, 202 verdi

Stato verificato

  • apps/platforms/src/lib/longevity/registro.ts: il registro semina solo QUESTIONARIO (20 voci q_*). PESI_ASSE cita 17 sotto-domini fisici che nessun test alimenta.
  • motore/test-fisici.ts: 20 funzioni verificate contro l'oracolo (tests/longevity/motore-fisici.test.ts), nessun chiamante in src/.
  • motore/index.ts calcolaSessione: legge solo le misure della sessione stessa (WHERE sessione_id = ?). Un check-up e un questionario sono due sessioni: il check-up da solo lascerebbe vuoti gli assi del questionario, e viceversa.
  • vista.ts sottoDominiPresenti (usata per mancano nel referto) guarda il join registro/misure della sola sessione.
  • vista.ts campiDelQuestionario filtra test_id con prefisso q_: i test fisici con prefisso f_ non entrano nel modulo del cliente.

Modifiche

  1. registro.ts: nuovo array FISICI: VoceRegistro[], seminato insieme a QUESTIONARIO da seedRegistro. Voci = misure grezze, prefisso f_, tipo_valore:'num', curva e params assenti, asse valorizzato (per l'interfaccia), sotto_dominio assente (il calcolo non passa da applicaCurva). range_min/range_max = limiti di plausibilità fisiologica larghi (commento: non soglie cliniche, servono a marcare errori di battitura). Elenco:
    f_peso_kg, f_handgrip_kg, f_pushup_rip, f_row_carico_kg, f_row_rip, f_squat_carico_kg, f_squat_rip, f_plank_sec, f_grasso_pct, f_muscolo_pct, f_whr, f_vo2max (ml/kg/min, diretto), f_mutt_vam (m/min), f_step_hr_recovery (bpm), f_spo2_pct, f_hrv_score (0-100, da Stress Index), f_pa_sistolica, f_pa_diastolica, f_hrr_1min_bpm, f_flamingo_cadute, f_sit_reach_cm, f_back_scratch_cm, f_spalla_outreach_deg, f_spalla_buckling_deg.
  2. Nuovo motore/fisici.ts: sottoDominiFisici(m: Map<string,number>, eta: number|null, sesso: Sesso): Record<string, number>. Mappa, identica a demo() dell'oracolo (tests/longevity/riferimento/isl_scoring_engine.py):
    • handgrip = scoreHandgrip(f_handgrip_kg, eta, sesso)
    • spinta = scorePushup(f_pushup_rip, eta, sesso)
    • trazione = scoreSollevamentoSuPeso(f_row_carico_kg, f_peso_kg, f_row_rip, TIERS_ROW_M, TIERS_ROW_F, sesso)
    • arti_inferiori = idem con f_squat_* e TIERS_SQUAT_*
    • core = plank = scorePlank(f_plank_sec, sesso) (doppio peso voluto, confermato da Nicola 22/08)
    • grasso, muscolo, whr = scoreGrassoPercento / scoreMuscoloPercento / scoreWhr
    • vo2max = scoreVo2max(v, eta, sesso) con v = f_vo2max, altrimenti vo2maxDaMutt(f_mutt_vam), altrimenti vo2maxDaStepTest(f_step_hr_recovery, sesso) — primo presente vince
    • wellness_tower_cardio = scoreSpo2(f_spo2_pct)
    • hrv = f_hrv_score così com'è, limitato 0-100
    • pressione = scorePressione(sist, diast) solo se entrambe presenti
    • hrr = scoreRecuperoCardiaco; flamingo, sit_and_reach, back_scratch = funzioni omonime; wellness_tower_shoulder = scoreMobilitaSpalla(outreach, buckling) solo se entrambi
      Regola: un sotto-dominio che manca di un ingresso (anche età null dove serve) non compare nel risultato. Mai un valore di ripiego.
      Fuori dal perimetro, e lo dichiara il commento: spirometria (calcolatore GLI assente), milfit, 2 km walk, sit-to-stand, trazione isometrica.
  3. motore/index.ts:
    a. Nuova misureValideAlla(db, sessioneId): Map<test_id, number> = per ogni test_id, il valore più recente dello stesso cliente fra le sessioni con (data < S.data) OR (data = S.data AND id <= S.id), fuori_range = 0, valore_num IS NOT NULL. Più recente = data DESC, id DESC. Costante esportata FINESTRA_GIORNI: number | null = null (null = nessun limite di età della misura), applicata se non null. Commento: decisione aperta per Nicola.
    b. Nuova sottoDominiDellaSessione(db, sessioneId): Record<string, number> = sotto-domini da curva (questionario, logica attuale del livello 1 ma sulle misure di 3a) + sottoDominiFisici con eta_alla_data della sessione e sesso da soggetti. Chiave in conflitto: vince il fisico (non ce ne sono oggi).
    c. calcolaSessione usa 3b per il livello 1. Livelli 2-4 invariati.
  4. vista.ts: sottoDominiPresenti diventa new Set(Object.keys(sottoDominiDellaSessione(db, sessioneId))). Eliminare la query attuale. Aggiornare il commento di ETICHETTE_SOTTO_DOMINIO.
  5. Commenti da aggiornare perché diventano falsi: testa di test-fisici.ts (⚠️ NON COLLEGATE), testa di scripts/seed-demo.ts (registro solo questionario).

TEST (vitest, apps/platforms/tests/longevity/)

  • motore-fisici-sessione.test.ts: caso demo() dell'oracolo (M, 38 anni, peso 90.3, handgrip 48, push-up 24, row 80x5, squat 140x5, plank 110, grasso 12.8, muscolo 48.8, whr 0.71, MUTT 230, SpO2 98, PA 114/81, HRR 15, flamingo 5, sit&reach 20, back scratch -5, spalla 180/180, hrv 68). Attesi: gli assi Forza & Struttura, Composizione Corporea, Stabilità & Mobilità Funzionale uguali all'oracolo. Valori attesi ricavati lanciando l'oracolo (python3), non a mano, con il comando scritto nel test. Cardio, Recupero ed Energia diversi per costruzione: l'oracolo usa segnaposto (spirometria 75, questionari 72/60/70, hrv 65 su Energia): scriverlo nel test.
  • sotto-dominio con un ingresso mancante → assente; età null → handgrip, spinta, vo2max assenti, plank presente.
  • combinazione: questionario il giorno D, check-up il giorno D+10 → il check-up ha sia gli assi del questionario sia quelli fisici; una sessione del giorno D-1 non vede le misure di D; misura fuori range ignorata e sostituita dalla precedente valida.
  • campiDelQuestionario non restituisce nessun f_*.
  • tutti i 202 test esistenti restano verdi; npx tsc --noEmit -p . senza errori nuovi.

NON TOCCARE

  • le funzioni di test-fisici.ts e curve.ts (porting verificato dell'oracolo).
  • anagrafica.ts, auth.ts, db.ts (schema invariato: nessuna colonna nuova).
  • punteggi congelati: nessuna ricalcolazione di sessioni esistenti.
  • apps/sito/.
ESITO: richiesta RIF: gitea:Adriano/InsanityLab-web#1 COSA: analisi (passo 1). Misure fisiche grezze nel registro, sotto-domini fisici calcolati con età e sesso, sessioni combinate nel punteggio. RAMO: lavoro/goldrake — clone di lavoro goldrake:~/lavoro/InsanityLab-web (NON /opt/docker/insanitylab/src, che è la produzione) BASE: 13d8cdd, apps/platforms: 27 file di test, 202 verdi ## Stato verificato - `apps/platforms/src/lib/longevity/registro.ts`: il registro semina solo `QUESTIONARIO` (20 voci `q_*`). `PESI_ASSE` cita 17 sotto-domini fisici che nessun test alimenta. - `motore/test-fisici.ts`: 20 funzioni verificate contro l'oracolo (`tests/longevity/motore-fisici.test.ts`), nessun chiamante in `src/`. - `motore/index.ts` `calcolaSessione`: legge solo le misure della sessione stessa (`WHERE sessione_id = ?`). Un check-up e un questionario sono due sessioni: il check-up da solo lascerebbe vuoti gli assi del questionario, e viceversa. - `vista.ts` `sottoDominiPresenti` (usata per `mancano` nel referto) guarda il join registro/misure della sola sessione. - `vista.ts` `campiDelQuestionario` filtra `test_id` con prefisso `q_`: i test fisici con prefisso `f_` non entrano nel modulo del cliente. ## Modifiche 1. `registro.ts`: nuovo array `FISICI: VoceRegistro[]`, seminato insieme a `QUESTIONARIO` da `seedRegistro`. Voci = **misure grezze**, prefisso `f_`, `tipo_valore:'num'`, `curva` e `params` assenti, `asse` valorizzato (per l'interfaccia), `sotto_dominio` **assente** (il calcolo non passa da `applicaCurva`). `range_min`/`range_max` = limiti di plausibilità fisiologica larghi (commento: non soglie cliniche, servono a marcare errori di battitura). Elenco: f_peso_kg, f_handgrip_kg, f_pushup_rip, f_row_carico_kg, f_row_rip, f_squat_carico_kg, f_squat_rip, f_plank_sec, f_grasso_pct, f_muscolo_pct, f_whr, f_vo2max (ml/kg/min, diretto), f_mutt_vam (m/min), f_step_hr_recovery (bpm), f_spo2_pct, f_hrv_score (0-100, da Stress Index), f_pa_sistolica, f_pa_diastolica, f_hrr_1min_bpm, f_flamingo_cadute, f_sit_reach_cm, f_back_scratch_cm, f_spalla_outreach_deg, f_spalla_buckling_deg. 2. Nuovo `motore/fisici.ts`: `sottoDominiFisici(m: Map<string,number>, eta: number|null, sesso: Sesso): Record<string, number>`. Mappa, identica a `demo()` dell'oracolo (`tests/longevity/riferimento/isl_scoring_engine.py`): - handgrip = scoreHandgrip(f_handgrip_kg, eta, sesso) - spinta = scorePushup(f_pushup_rip, eta, sesso) - trazione = scoreSollevamentoSuPeso(f_row_carico_kg, f_peso_kg, f_row_rip, TIERS_ROW_M, TIERS_ROW_F, sesso) - arti_inferiori = idem con f_squat_* e TIERS_SQUAT_* - core = plank = scorePlank(f_plank_sec, sesso) (doppio peso voluto, confermato da Nicola 22/08) - grasso, muscolo, whr = scoreGrassoPercento / scoreMuscoloPercento / scoreWhr - vo2max = scoreVo2max(v, eta, sesso) con v = f_vo2max, altrimenti vo2maxDaMutt(f_mutt_vam), altrimenti vo2maxDaStepTest(f_step_hr_recovery, sesso) — primo presente vince - wellness_tower_cardio = scoreSpo2(f_spo2_pct) - hrv = f_hrv_score così com'è, limitato 0-100 - pressione = scorePressione(sist, diast) solo se entrambe presenti - hrr = scoreRecuperoCardiaco; flamingo, sit_and_reach, back_scratch = funzioni omonime; wellness_tower_shoulder = scoreMobilitaSpalla(outreach, buckling) solo se entrambi Regola: un sotto-dominio che manca di un ingresso (anche età null dove serve) **non compare** nel risultato. Mai un valore di ripiego. Fuori dal perimetro, e lo dichiara il commento: spirometria (calcolatore GLI assente), milfit, 2 km walk, sit-to-stand, trazione isometrica. 3. `motore/index.ts`: a. Nuova `misureValideAlla(db, sessioneId): Map<test_id, number>` = per ogni test_id, il valore più recente **dello stesso cliente** fra le sessioni con `(data < S.data) OR (data = S.data AND id <= S.id)`, `fuori_range = 0`, `valore_num IS NOT NULL`. Più recente = data DESC, id DESC. Costante esportata `FINESTRA_GIORNI: number | null = null` (null = nessun limite di età della misura), applicata se non null. Commento: decisione aperta per Nicola. b. Nuova `sottoDominiDellaSessione(db, sessioneId): Record<string, number>` = sotto-domini da curva (questionario, logica attuale del livello 1 ma sulle misure di 3a) + `sottoDominiFisici` con `eta_alla_data` della sessione e `sesso` da `soggetti`. Chiave in conflitto: vince il fisico (non ce ne sono oggi). c. `calcolaSessione` usa 3b per il livello 1. Livelli 2-4 invariati. 4. `vista.ts`: `sottoDominiPresenti` diventa `new Set(Object.keys(sottoDominiDellaSessione(db, sessioneId)))`. Eliminare la query attuale. Aggiornare il commento di `ETICHETTE_SOTTO_DOMINIO`. 5. Commenti da aggiornare perché diventano falsi: testa di `test-fisici.ts` (⚠️ NON COLLEGATE), testa di `scripts/seed-demo.ts` (registro solo questionario). ## TEST (vitest, `apps/platforms/tests/longevity/`) - `motore-fisici-sessione.test.ts`: caso `demo()` dell'oracolo (M, 38 anni, peso 90.3, handgrip 48, push-up 24, row 80x5, squat 140x5, plank 110, grasso 12.8, muscolo 48.8, whr 0.71, MUTT 230, SpO2 98, PA 114/81, HRR 15, flamingo 5, sit&reach 20, back scratch -5, spalla 180/180, hrv 68). Attesi: gli assi **Forza & Struttura**, **Composizione Corporea**, **Stabilità & Mobilità Funzionale** uguali all'oracolo. Valori attesi ricavati lanciando l'oracolo (`python3`), non a mano, con il comando scritto nel test. Cardio, Recupero ed Energia diversi per costruzione: l'oracolo usa segnaposto (spirometria 75, questionari 72/60/70, hrv 65 su Energia): scriverlo nel test. - sotto-dominio con un ingresso mancante → assente; età null → handgrip, spinta, vo2max assenti, plank presente. - combinazione: questionario il giorno D, check-up il giorno D+10 → il check-up ha sia gli assi del questionario sia quelli fisici; una sessione del giorno D-1 non vede le misure di D; misura fuori range ignorata e sostituita dalla precedente valida. - `campiDelQuestionario` non restituisce nessun `f_*`. - tutti i 202 test esistenti restano verdi; `npx tsc --noEmit -p .` senza errori nuovi. ## NON TOCCARE - le funzioni di `test-fisici.ts` e `curve.ts` (porting verificato dell'oracolo). - `anagrafica.ts`, `auth.ts`, `db.ts` (schema invariato: nessuna colonna nuova). - punteggi congelati: nessuna ricalcolazione di sessioni esistenti. - `apps/sito/`.
Author
Owner

Soluzione

ESITO: fatto (sul ramo lavoro/goldrake, NON in main e non in produzione)
COMMIT: 48910d8 registro FISICI + motore/fisici.ts · 4968b75 misureValideAlla, sottoDominiDellaSessione, calcolaSessione e vista agganciati · af8afeb spareggio m.id DESC e nota sulla conferma dei fuori range (#7)
FILE: apps/platforms/src/lib/longevity/registro.ts (24 voci f_*), motore/fisici.ts (nuovo, sottoDominiFisici), motore/index.ts, vista.ts, scripts/seed-demo.ts (commento), tests/longevity/motore-fisici-sessione.test.ts (10 test)
REGOLA NUOVA: il punteggio di una sessione usa per ogni test la misura valida più recente del cliente fino a quella data (FINESTRA_GIORNI = null, nessun limite: decisione aperta per Nicola).
PROVA: npx vitest run → 212 passed (202 prima); tsc --noEmit → 0 errori. Caso demo() dell'oracolo: Forza, Composizione, Stabilità identici.
FUORI PERIMETRO: spirometria (manca il calcolatore GLI), milfit, 2 km walk, sit-to-stand, trazione isometrica.
MANCA: merge in main e deploy, insieme al gestionale (#2).

**Soluzione** ESITO: fatto (sul ramo lavoro/goldrake, NON in main e non in produzione) COMMIT: 48910d8 registro FISICI + motore/fisici.ts · 4968b75 misureValideAlla, sottoDominiDellaSessione, calcolaSessione e vista agganciati · af8afeb spareggio m.id DESC e nota sulla conferma dei fuori range (#7) FILE: apps/platforms/src/lib/longevity/registro.ts (24 voci f_*), motore/fisici.ts (nuovo, sottoDominiFisici), motore/index.ts, vista.ts, scripts/seed-demo.ts (commento), tests/longevity/motore-fisici-sessione.test.ts (10 test) REGOLA NUOVA: il punteggio di una sessione usa per ogni test la misura valida più recente del cliente fino a quella data (FINESTRA_GIORNI = null, nessun limite: decisione aperta per Nicola). PROVA: npx vitest run → 212 passed (202 prima); tsc --noEmit → 0 errori. Caso demo() dell'oracolo: Forza, Composizione, Stabilità identici. FUORI PERIMETRO: spirometria (manca il calcolatore GLI), milfit, 2 km walk, sit-to-stand, trazione isometrica. MANCA: merge in main e deploy, insieme al gestionale (#2).
Adriano removed the assegnata:goldrake label 2026-09-26 12:41:05 +00:00
Author
Owner

ESITO: fatto
COSA: in produzione su piattaforme.tielogic.xyz, 2026-09-26 13:17 UTC. main 12b2e91.
PROVA: docker compose up -d --build → container Up; migra() → colonne verifica/verificata_da/verificata_at presenti; registro_test f_ = 24; misure 20 prima e dopo.
BACKUP: /opt/docker/insanitylab-platforms/backup/*-pre-gestionale (sqlite3 .backup, integrity ok).

ESITO: fatto COSA: in produzione su piattaforme.tielogic.xyz, 2026-09-26 13:17 UTC. main 12b2e91. PROVA: docker compose up -d --build → container Up; migra() → colonne verifica/verificata_da/verificata_at presenti; registro_test f_ = 24; misure 20 prima e dopo. BACKUP: /opt/docker/insanitylab-platforms/backup/*-pre-gestionale (sqlite3 .backup, integrity ok).
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Adriano/InsanityLab-web#1