motore: chiude i tre buchi laterali della barriera dell'insufficiente
- eta_alla_data NULL non usa più il ripiego a zero: senza età la Fitness Age è null, non un'età di forma negativa dichiarata valida. Gli assi e i macro-score restano calcolabili (non dipendono dall'età). - applicaCurva valida i parametri richiesti da ciascuna curva prima di applicarla: un registro con params vuoti/incompleti dà null, mai il punteggio pieno che dava lerp con estremi indefiniti. - calcolaSessione esclude le misure fuori_range=1 (§9 della spec): non entrano nello score finché non esiste la colonna di conferma (§12, punto aperto). - leggiScore(db, sessioneId): legge la tabella score ritipata come Punteggio, con l'ultimo calcolo per tipo+elemento esplicito nella query (MAX(id)), non l'ordine naturale delle righe — salvaScore non sovrascrive di proposito (la storia degli score si tiene, discende dal congelamento della §6). - documentata la seconda divergenza dall'oracolo, mai scritta finora: la Fitness Age esclude gli assi insufficienti, l'oracolo li include comunque. Comportamento giusto, ma cambia il numero (25,0 contro 34,8 sullo stesso profilo parziale). - nuovo test che prova che i pesi di due model_version diverse restano separati (pesiDi filtra su model_version, non li fonde). Ogni fix verificato in TDD (test rosso prima, verde dopo) e ri-rotto a mano per confermare che discrimina davvero (vedi report). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XhLbMQ1q7wHwJSykgXRwQF
This commit is contained in:
@@ -10,14 +10,31 @@
|
||||
* peso si ridistribuisce su quelli disponibili; se la copertura di peso
|
||||
* disponibile scende sotto `COPERTURA_MINIMA`, l'elemento e' insufficiente.
|
||||
*
|
||||
* UNICA DIVERGENZA VOLUTA dall'oracolo: nell'oracolo `aggregate()` restituisce
|
||||
* il punteggio pieno anche quando lo dichiara insufficiente (la docstring
|
||||
* dice il contrario di cio' che il codice fa) - chi consuma deve ricordarsi
|
||||
* di guardare lo stato, e prima o poi qualcuno non lo fa. Qui il tipo
|
||||
* `Punteggio` chiude la trappola: nel ramo 'insufficiente' il campo `valore`
|
||||
* non esiste, quindi la regola di prodotto - un asse con dati insufficienti
|
||||
* si mostra tratteggiato, mai con un numero pieno fasullo - e' impossibile
|
||||
* da violare per distrazione. Tutto il resto e' fedele all'oracolo.
|
||||
* DUE DIVERGENZE VOLUTE dall'oracolo:
|
||||
*
|
||||
* 1) Nell'oracolo `aggregate()` restituisce il punteggio pieno anche quando lo
|
||||
* dichiara insufficiente (la docstring dice il contrario di cio' che il
|
||||
* codice fa) - chi consuma deve ricordarsi di guardare lo stato, e prima o
|
||||
* poi qualcuno non lo fa. Qui il tipo `Punteggio` chiude la trappola: nel
|
||||
* ramo 'insufficiente' il campo `valore` non esiste, quindi la regola di
|
||||
* prodotto - un asse con dati insufficienti si mostra tratteggiato, mai
|
||||
* con un numero pieno fasullo - e' impossibile da violare per distrazione.
|
||||
*
|
||||
* 2) Nel calcolo della Fitness Age (vedi `motore/index.ts`, `valoreAsse`)
|
||||
* escludiamo gli assi 'insufficiente' dal composito: `cardio`,
|
||||
* `forza_resto`, `composizione` e `stabilita` entrano solo se il rispettivo
|
||||
* asse e' 'ok'. L'oracolo invece li include comunque - per lo stesso motivo
|
||||
* del punto 1: `compute_axis` restituisce uno `score` numerico anche
|
||||
* quando lo stato e' "insufficiente", e `compute_fitness_age` lo riceve e
|
||||
* lo usa senza controllare lo stato. E' il comportamento GIUSTO (un asse
|
||||
* sotto soglia non e' un dato affidabile da far pesare sull'eta biologica),
|
||||
* ma cambia il numero: su un profilo con assi parzialmente insufficienti,
|
||||
* verificato a mano, la nostra Fitness Age e' 25,0 contro 34,8 dell'oracolo
|
||||
* - quasi dieci anni di scarto. Chi confronta il nostro referto col Python
|
||||
* del cliente su un profilo parziale concludera' che il porting e' rotto:
|
||||
* non lo e', diverge di proposito qui.
|
||||
*
|
||||
* Tutto il resto e' fedele all'oracolo.
|
||||
*/
|
||||
|
||||
import { COPERTURA_MINIMA } from '../db';
|
||||
@@ -96,9 +113,17 @@ export function calcolaMacro(pesiMacro: Record<string, number>, assi: Record<str
|
||||
* composito di sei elementi (handgrip e HRV isolati, non tramite l'intero
|
||||
* asse) da cui `eta - (composito - 50) * 0.4`. Se il composito e'
|
||||
* insufficiente non c'e' Fitness Age da mostrare.
|
||||
*
|
||||
* `etaAnagrafica` e' `number | null` perche' `sessioni.eta_alla_data` e'
|
||||
* nullable nello schema: l'apertura di una sessione e il salvataggio di un
|
||||
* questionario accettano l'eta come opzionale. Senza eta la Fitness Age NON
|
||||
* ESISTE - deve restituire `null`, mai un numero calcolato con un ripiego
|
||||
* (es. 0), che produrrebbe un'eta di forma negativa dichiarata valida. Gli
|
||||
* assi e i macro-score non dipendono dall'eta e restano calcolabili a monte:
|
||||
* solo questo ultimo livello si ferma.
|
||||
*/
|
||||
export function calcolaFitnessAge(
|
||||
etaAnagrafica: number,
|
||||
etaAnagrafica: number | null,
|
||||
pesi: Record<string, number>,
|
||||
voci: Record<string, number | null>
|
||||
): { fitnessAge: number | null; composito: Punteggio } {
|
||||
@@ -107,7 +132,7 @@ export function calcolaFitnessAge(
|
||||
peso,
|
||||
}));
|
||||
const composito = aggrega(vociPesate);
|
||||
if (composito.stato === 'insufficiente') {
|
||||
if (composito.stato === 'insufficiente' || etaAnagrafica === null) {
|
||||
return { fitnessAge: null, composito };
|
||||
}
|
||||
const fitnessAge = arrotonda1(etaAnagrafica - (composito.valore - 50) * 0.4);
|
||||
|
||||
Reference in New Issue
Block a user