feat: storia diete, schedule allenamento, frequenza pasti (PRD §5.1)

Tre leve aggiuntive che migliorano la personalizzazione del piano dopo
le tre del commit precedente (BF%, deficit, condizioni mediche).

1. Storia delle diete precedenti (adattamento metabolico)
   - Profile.dietHistory ∈ {NONE,SOME,EXTENSIVE,SEVERE} cifrato at-rest
   - bmrAdjustForDietHistory: 0.97 / 0.93 / 0.85 — aggiustamento BMR per
     riflettere l'adattamento metabolico osservato in studi a lungo termine
     (Fothergill 2016 Biggest Loser, Rosenbaum 2008)
   - Select in ProfileForm con copy diretto sulla "questione yo-yo"

2. Schedule allenamento (kcal extra training-day)
   - Preferences.trainingDays Int[] (0=Lun..6=Dom)
   - Preferences.trainingType ∈ {CARDIO,STRENGTH,MIXED,SPORT}
   - Preferences.sessionMinutes Int?
   - extraKcalForSession: equazione MET (Ainsworth 2011) con MET prevalenti
     8/5/6/7 a tipo. Output arrotondato a 25 kcal per evitare false-precisioni
   - Plan generation aggiunge l'extra solo nei training days
   - Chips Lun-Dom + select tipo + input durata in PreferencesForm

3. Frequenza pasti preferita
   - Preferences.mealsPerDay Int? (1..4)
   - mealShareForFrequency: 1=solo cena, 2=pranzo+cena, 3=no spuntino, 4=default
   - Quando mealsPerDay è impostato e non c'è fastingProtocol attivo,
     sostituisce la share del protocollo (il digiuno IF ha precedenza)
   - Slot a share=0 saltati alla creazione (niente colazione/spuntino "vuoti")

Schema migration 20260429220XXX_add_lifestyle_fields.

Smoke verificato via API: con mealsPerDay=3 e nessun protocollo IF, ogni
giorno del piano ha esattamente 3 pasti (Colazione+Pranzo+Cena, niente
Spuntino). 54/54 unit test verdi (11 nuovi).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
lucianoandClaude Opus 4.7 committed 2026-04-29 22:56:41 +02:00
1 parent 9401497f03
commit a583434c42
20 files changed
+460 -31

No files matched your search

@@ -0,0 +1,22 @@
import { describe, expect, it } from 'vitest';
import { bmrAdjustForDietHistory } from './diet-history.js';
describe('bmrAdjustForDietHistory', () => {
it('NONE → 1.0', () => {
expect(bmrAdjustForDietHistory('NONE')).toBe(1);
});
it('SOME → 0.97', () => {
expect(bmrAdjustForDietHistory('SOME')).toBe(0.97);
});
it('EXTENSIVE → 0.93', () => {
expect(bmrAdjustForDietHistory('EXTENSIVE')).toBe(0.93);
});
it('SEVERE → 0.85', () => {
expect(bmrAdjustForDietHistory('SEVERE')).toBe(0.85);
});
it('null/undefined → 1.0', () => {
expect(bmrAdjustForDietHistory(null)).toBe(1);
expect(bmrAdjustForDietHistory(undefined)).toBe(1);
});
});
@@ -0,0 +1,25 @@
// PRD §5.1 — Adattamento metabolico da diete restrittive precedenti.
//
// Riferimenti: Fothergill 2016 (Biggest Loser, BMR ~500 kcal sotto la
// previsione 6 anni dopo), Rosenbaum 2008. Le persone con storia di diete
// pesanti hanno BMR misurato 5-15% sotto Mifflin/Katch — applichiamo un
// moltiplicatore conservativo per non spingere il deficit oltre il dovuto.
export const DIET_HISTORIES = ['NONE', 'SOME', 'EXTENSIVE', 'SEVERE'] as const;
export type DietHistory = (typeof DIET_HISTORIES)[number];
export function bmrAdjustForDietHistory(history: DietHistory | null | undefined): number {
switch (history) {
case 'SOME':
return 0.97; // 1-2 diete blande negli ultimi 3 anni
case 'EXTENSIVE':
return 0.93; // diete frequenti / yo-yo conclamato
case 'SEVERE':
return 0.85; // semi-fame prolungata, ricoveri, anoressia in remissione
case 'NONE':
case null:
case undefined:
default:
return 1;
}
}
+4
View File
@@ -2,6 +2,8 @@ import { z } from 'zod';
import { MEDICAL_CONDITIONS } from '../medical/conditions.js';
import { DIET_HISTORIES } from './diet-history.js';
export const GENDERS = ['MALE', 'FEMALE', 'OTHER'] as const;
export const ACTIVITY_LEVELS = ['SEDENTARY', 'LIGHT', 'MODERATE', 'INTENSE'] as const;
@@ -23,6 +25,8 @@ export const profileInputSchema = z.object({
neckCm: z.coerce.number().min(20).max(60).optional(),
waistCm: z.coerce.number().min(40).max(200).optional(),
hipsCm: z.coerce.number().min(40).max(200).optional(),
// Storia delle diete restrittive precedenti (vedi diet-history.ts).
dietHistory: z.enum(DIET_HISTORIES).optional(),
});
export type ProfileInput = z.input<typeof profileInputSchema>;