mitai-jinkendo/.claude/docs/working/phase-0c-placeholder-migration-analysis.md
Lars 052ba195cc
All checks were successful
Deploy Development / deploy (push) Successful in 54s
Build Test / pytest-backend (push) Successful in 4s
Build Test / lint-backend (push) Successful in 1s
Build Test / build-frontend (push) Successful in 15s
feat: Update placeholder metadata and nutrition metrics
- Adjusted the total number of placeholders from 116 to 114 across various documentation and code files to reflect the current state of the system.
- Enhanced TDEE calculation logic in `nutrition_metrics.py` to prioritize Mifflin–St Jeor BMR with PAL when demographic data is available, with a fallback to a weight-based estimate.
- Updated placeholder registrations to ensure consistency with the new metadata structure and improved data handling.
- Revised documentation to clarify the authoritative source of placeholder metadata and the implications of the changes on existing functionalities.

These updates improve the accuracy and consistency of the placeholder system and enhance the nutritional assessment capabilities within the application.
2026-04-11 21:11:05 +02:00

16 KiB
Raw Blame History

Phase 0c: Placeholder Migration Analysis

Erstellt: 28. März 2026 Zweck: Analyse welche Platzhalter zu Data Layer migriert werden müssen


Gesamt-Übersicht

Aktuelle Platzhalter: 114 (PLACEHOLDER_MAP / Registry) Nach Phase 0c Migration:

  • Bleiben einfach (kein Data Layer): 8 Platzhalter
  • 🔄 Gehen zu Data Layer: 108 Platzhalter

Kategorisierung: BLEIBEN EINFACH (8 Platzhalter)

Diese Platzhalter bleiben im KI Layer (placeholder_resolver.py) weil sie:

  • Keine Berechnungen durchführen
  • Keine Daten-Aggregation benötigen
  • Einfache Getter oder Konstanten sind

Zeitraum (4 Platzhalter)

'{{datum_heute}}': lambda pid: datetime.now().strftime('%d.%m.%Y')
'{{zeitraum_7d}}': lambda pid: 'letzte 7 Tage'
'{{zeitraum_30d}}': lambda pid: 'letzte 30 Tage'
'{{zeitraum_90d}}': lambda pid: 'letzte 90 Tage'

Begründung: Konstanten oder einfache Datum-Formatierung. Kein Data Layer nötig.

Profil - Basis (4 Platzhalter)

'{{name}}': lambda pid: get_profile_data(pid).get('name', 'Nutzer')
'{{age}}': lambda pid: calculate_age(get_profile_data(pid).get('dob'))
'{{height}}': lambda pid: str(get_profile_data(pid).get('height', 'unbekannt'))
'{{geschlecht}}': lambda pid: 'männlich' if get_profile_data(pid).get('sex') == 'm' else 'weiblich'

Begründung: Direkte Getter aus profiles Tabelle. Keine Aggregation.


GEHEN ZU DATA LAYER (108 Platzhalter)

1. Körper (20 Platzhalter) → data_layer.body_metrics

Basis-Metriken (8):

'{{weight_aktuell}}'         get_weight_trend_data()['last_value']
'{{weight_trend}}'           get_weight_trend_data() (formatiert)
'{{kf_aktuell}}'             get_body_composition_data()['body_fat_pct'][-1]
'{{bmi}}'                    get_body_composition_data() (berechnet)
'{{caliper_summary}}'        get_caliper_summary_data()
'{{circ_summary}}'           get_circumference_summary()
'{{goal_weight}}'            get_active_goals() (filtered)
'{{goal_bf_pct}}'            get_active_goals() (filtered)

Phase 0b - Advanced Body (12):

'{{weight_7d_median}}'       get_weight_trend_data()['rolling_median_7d'][-1]
'{{weight_28d_slope}}'       get_weight_trend_data()['slope_28d']
'{{weight_90d_slope}}'       get_weight_trend_data()['slope_90d']
'{{fm_28d_change}}'          get_body_composition_data()['fm_delta_28d']
'{{lbm_28d_change}}'         get_body_composition_data()['lbm_delta_28d']
'{{waist_28d_delta}}'        get_circumference_summary()['changes']['waist_28d']
'{{hip_28d_delta}}'          get_circumference_summary()['changes']['hip_28d']
'{{chest_28d_delta}}'        get_circumference_summary()['changes']['chest_28d']
'{{arm_28d_delta}}'          get_circumference_summary()['changes']['arm_28d']
'{{thigh_28d_delta}}'        get_circumference_summary()['changes']['thigh_28d']
'{{waist_hip_ratio}}'        get_circumference_summary()['ratios']['waist_to_hip']
'{{recomposition_quadrant}}' get_body_composition_data()['recomposition_score']

Data Layer Funktionen benötigt:

  • get_weight_trend_data(profile_id, days=90)
  • get_body_composition_data(profile_id, days=90)
  • get_circumference_summary(profile_id, days=90)
  • get_caliper_summary_data(profile_id, days=90)

2. Ernährung (14 Platzhalter) → data_layer.nutrition_metrics

Basis-Metriken (7):

'{{kcal_avg}}'               get_energy_balance_data()['avg_intake']
'{{protein_avg}}'            get_protein_adequacy_data()['avg_protein_g']
'{{carb_avg}}'               get_macro_distribution_data()['avg_carbs_g']
'{{fat_avg}}'                get_macro_distribution_data()['avg_fat_g']
'{{nutrition_days}}'         get_energy_balance_data()['data_points']
'{{protein_ziel_low}}'       get_protein_adequacy_data()['target_protein_g'] (low)
'{{protein_ziel_high}}'      get_protein_adequacy_data()['target_protein_g'] (high)

Phase 0b - Advanced Nutrition (7):

'{{energy_balance_7d}}'      get_energy_balance_data()['avg_net']
'{{energy_deficit_surplus}}' get_energy_balance_data()['deficit_surplus_avg']
'{{protein_g_per_kg}}'       get_protein_adequacy_data()['avg_protein_per_kg']
'{{protein_days_in_target}}' get_protein_adequacy_data()['adherence_pct']
'{{protein_adequacy_28d}}'   get_protein_adequacy_data()['adherence_score']
'{{macro_consistency_score}}' get_macro_distribution_data()['balance_score']
'{{intake_volatility}}'      get_macro_distribution_data()['variability']

Data Layer Funktionen benötigt:

  • get_protein_adequacy_data(profile_id, days=28, goal_mode=None)
  • get_energy_balance_data(profile_id, days=28)
  • get_macro_distribution_data(profile_id, days=28)

3. Training (16 Platzhalter) → data_layer.activity_metrics

Basis-Metriken (3):

'{{activity_summary}}'       get_training_volume_data()['weekly_totals'] (formatted)
'{{activity_detail}}'        get_training_volume_data()['by_type'] (formatted)
'{{trainingstyp_verteilung}}' get_activity_quality_distribution()

Phase 0b - Advanced Activity (13):

'{{training_minutes_week}}'  get_training_volume_data()['weekly_totals'][0]['duration_min']
'{{training_frequency_7d}}'  get_training_volume_data()['weekly_totals'][0]['sessions']
'{{quality_sessions_pct}}'   get_activity_quality_distribution()['high_quality_pct']
'{{ability_balance_strength}}'  get_ability_balance_data()['abilities']['strength']
'{{ability_balance_endurance}}' get_ability_balance_data()['abilities']['cardio']
'{{ability_balance_mental}}'    get_ability_balance_data()['abilities']['mental']
'{{ability_balance_coordination}}' get_ability_balance_data()['abilities']['coordination']
'{{ability_balance_mobility}}'   get_ability_balance_data()['abilities']['mobility']
'{{proxy_internal_load_7d}}' get_training_volume_data()['strain']
'{{monotony_score}}'         get_training_volume_data()['monotony']
'{{strain_score}}'           get_training_volume_data()['strain']
'{{rest_day_compliance}}'    get_recovery_score_data()['components']['rest_compliance']['score']
'{{vo2max_trend_28d}}'       get_vitals_baseline_data()['vo2_max']['trend']

Data Layer Funktionen benötigt:

  • get_training_volume_data(profile_id, weeks=4)
  • get_activity_quality_distribution(profile_id, days=28)
  • get_ability_balance_data(profile_id, weeks=4)

4. Schlaf & Erholung (10 Platzhalter) → data_layer.recovery_metrics

Basis-Metriken (3):

'{{sleep_avg_duration}}'     get_sleep_regularity_data()['avg_duration_h']
'{{sleep_avg_quality}}'      get_sleep_regularity_data()['avg_quality']
'{{rest_days_count}}'        get_recovery_score_data()['components']['rest_compliance']['rest_days']

Phase 0b - Advanced Recovery (7):

'{{hrv_vs_baseline_pct}}'    get_vitals_baseline_data()['hrv']['deviation_pct']
'{{rhr_vs_baseline_pct}}'    get_vitals_baseline_data()['rhr']['deviation_pct']
'{{sleep_avg_duration_7d}}'  get_sleep_regularity_data()['avg_duration_h']
'{{sleep_debt_hours}}'       get_sleep_regularity_data()['sleep_debt_h']
'{{sleep_regularity_proxy}}' get_sleep_regularity_data()['regularity_score']
'{{recent_load_balance_3d}}' get_recovery_score_data()['load_balance']
'{{sleep_quality_7d}}'       get_sleep_regularity_data()['avg_quality']

Data Layer Funktionen benötigt:

  • get_recovery_score_data(profile_id, days=7)
  • get_sleep_regularity_data(profile_id, days=28)
  • get_vitals_baseline_data(profile_id, days=7)

5. Vitalwerte (3 Platzhalter) → data_layer.health_metrics

'{{vitals_avg_hr}}'          get_vitals_baseline_data()['rhr']['current']
'{{vitals_avg_hrv}}'         get_vitals_baseline_data()['hrv']['current']
'{{vitals_vo2_max}}'         get_vitals_baseline_data()['vo2_max']['current']

Data Layer Funktionen benötigt:

  • get_vitals_baseline_data(profile_id, days=7) (bereits in recovery)

6. Scores (6 Platzhalter) → Diverse Module

'{{goal_progress_score}}'    get_goal_progress_data()  goals.py
'{{body_progress_score}}'    get_body_composition_data()  body_metrics.py
'{{nutrition_score}}'        get_protein_adequacy_data()  nutrition_metrics.py
'{{activity_score}}'         get_training_volume_data()  activity_metrics.py
'{{recovery_score}}'         get_recovery_score_data()['score']  recovery_metrics.py
'{{data_quality_score}}'     get_data_quality_score()  utils.py (NEW)

Hinweis: Scores nutzen bestehende Data Layer Funktionen, nur Formatierung nötig.


7. Top Goals/Focus (5 Platzhalter) → data_layer.goals

'{{top_goal_name}}'          get_active_goals()[0]['name']
'{{top_goal_progress_pct}}'  get_active_goals()[0]['progress_pct']
'{{top_goal_status}}'        get_active_goals()[0]['status']
'{{top_focus_area_name}}'    get_weighted_focus_areas()[0]['name']
'{{top_focus_area_progress}}' get_weighted_focus_areas()[0]['progress']

Data Layer Funktionen benötigt:

  • get_active_goals(profile_id) (already exists from Phase 0b)
  • get_weighted_focus_areas(profile_id) (already exists from Phase 0b)

8. Category Scores (14 Platzhalter) → Formatierung nur

'{{focus_cat_körper_progress}}'      _format_from_aggregated_data()
'{{focus_cat_körper_weight}}'        _format_from_aggregated_data()
'{{focus_cat_ernährung_progress}}'   _format_from_aggregated_data()
'{{focus_cat_ernährung_weight}}'     _format_from_aggregated_data()
# ... (7 Kategorien × 2 = 14 total)

Hinweis: Diese nutzen bereits aggregierte Daten aus Phase 0b. Migration: Nur KI Layer Formatierung, Data Layer nicht nötig (Daten kommen aus anderen Funktionen).


9. Korrelationen (7 Platzhalter) → data_layer.correlations

'{{correlation_energy_weight_lag}}'   get_correlation_data(pid, 'energy', 'weight')
'{{correlation_protein_lbm}}'         get_correlation_data(pid, 'protein', 'lbm')
'{{correlation_load_hrv}}'            get_correlation_data(pid, 'load', 'hrv')
'{{correlation_load_rhr}}'            get_correlation_data(pid, 'load', 'rhr')
'{{correlation_sleep_recovery}}'      get_correlation_data(pid, 'sleep', 'recovery')
'{{plateau_detected}}'                detect_plateau(pid, 'weight')
'{{top_drivers}}'                     get_top_drivers(pid)

Data Layer Funktionen benötigt:

  • get_correlation_data(profile_id, metric_a, metric_b, days=90, max_lag=7)
  • detect_plateau(profile_id, metric, days=28)
  • get_top_drivers(profile_id) (NEW - identifies top correlations)

10. JSON/Markdown (8 Platzhalter) → Formatierung nur

'{{active_goals_json}}'              json.dumps(get_active_goals(pid))
'{{active_goals_md}}'                format_as_markdown(get_active_goals(pid))
'{{focus_areas_weighted_json}}'      json.dumps(get_weighted_focus_areas(pid))
'{{focus_areas_weighted_md}}'        format_as_markdown(get_weighted_focus_areas(pid))
'{{focus_area_weights_json}}'        json.dumps(get_focus_area_weights(pid))
'{{top_3_focus_areas}}'              format_top_3(get_weighted_focus_areas(pid))
'{{top_3_goals_behind_schedule}}'    format_goals_behind(get_active_goals(pid))
'{{top_3_goals_on_track}}'           format_goals_on_track(get_active_goals(pid))

Hinweis: Diese nutzen bereits existierende Data Layer Funktionen. Migration: Nur KI Layer Formatierung (json.dumps, markdown, etc.).


Data Layer Funktionen - Zusammenfassung

Neue Funktionen zu erstellen (Phase 0c):

body_metrics.py (4 Funktionen):

  • get_weight_trend_data()
  • get_body_composition_data()
  • get_circumference_summary()
  • get_caliper_summary_data()

nutrition_metrics.py (3 Funktionen):

  • get_protein_adequacy_data()
  • get_energy_balance_data()
  • get_macro_distribution_data()

activity_metrics.py (3 Funktionen):

  • get_training_volume_data()
  • get_activity_quality_distribution()
  • get_ability_balance_data()

recovery_metrics.py (2 Funktionen):

  • get_recovery_score_data()
  • get_sleep_regularity_data()

health_metrics.py (2 Funktionen):

  • get_vitals_baseline_data()
  • get_blood_pressure_data() (aus Spec)

goals.py (3 Funktionen):

  • get_active_goals() (exists from Phase 0b)
  • get_weighted_focus_areas() (exists from Phase 0b)
  • get_goal_progress_data() (aus Spec)

correlations.py (3 Funktionen):

  • get_correlation_data()
  • detect_plateau()
  • 🆕 get_top_drivers() (NEW - not in spec)

utils.py (Shared):

  • calculate_confidence()
  • calculate_baseline()
  • detect_outliers()
  • aggregate_data()
  • serialize_dates()
  • 🆕 get_data_quality_score() (NEW)

Total neue Funktionen: 20 (aus Spec) + 2 (zusätzlich) = 22 Data Layer Funktionen


Migration-Aufwand pro Kategorie

Kategorie Platzhalter Data Layer Funcs Aufwand Priorität
Körper 20 4 3-4h High
Ernährung 14 3 2-3h High
Training 16 3 3-4h Medium
Recovery 10 2 2-3h Medium
Vitalwerte 3 1 (shared) 0.5h Low
Scores 6 0 (use others) 1h Low
Goals/Focus 5 0 (exists) 0.5h Low
Categories 14 0 (formatting) 1h Low
Korrelationen 7 3 2-3h Medium
JSON/Markdown 8 0 (formatting) 0.5h Low
TOTAL 108 22 16-22h -

KI Layer Refactoring-Muster

VORHER (Phase 0b):

def get_latest_weight(profile_id: str) -> str:
    """Returns latest weight with SQL + formatting"""
    with get_db() as conn:
        cur = get_cursor(conn)
        cur.execute("""
            SELECT weight FROM weight_log
            WHERE profile_id = %s
            ORDER BY date DESC LIMIT 1
        """, (profile_id,))
        row = cur.fetchone()
        if not row:
            return "nicht verfügbar"
        return f"{row['weight']:.1f} kg"

PLACEHOLDER_MAP = {
    '{{weight_aktuell}}': get_latest_weight,
}

NACHHER (Phase 0c):

from data_layer.body_metrics import get_weight_trend_data

def resolve_weight_aktuell(profile_id: str) -> str:
    """Returns latest weight (formatted for KI)"""
    data = get_weight_trend_data(profile_id, days=7)

    if data['confidence'] == 'insufficient':
        return "nicht verfügbar"

    return f"{data['last_value']:.1f} kg"

PLACEHOLDER_MAP = {
    '{{weight_aktuell}}': resolve_weight_aktuell,
}

Reduzierung: Von ~15 Zeilen (SQL + Logic) zu ~7 Zeilen (Call + Format)


Erwartetes Ergebnis nach Phase 0c

Zeilen-Reduktion:

  • placeholder_resolver.py:
    • Vorher: ~1200 Zeilen
    • Nachher: ~400 Zeilen (67% Reduktion)

Code-Qualität:

  • Keine SQL queries in placeholder_resolver.py
  • Keine Berechnungslogik in placeholder_resolver.py
  • Nur Formatierung für KI-Consumption

Wiederverwendbarkeit:

  • 22 Data Layer Funktionen nutzbar für:
    • KI Layer (108 Platzhalter)
    • Charts Layer (10+ Charts)
    • API Endpoints (beliebig erweiterbar)

Checkliste: Migration pro Platzhalter

Für jeden der 108 Platzhalter:

[ ] Data Layer Funktion existiert
[ ] KI Layer ruft Data Layer Funktion auf
[ ] Formatierung für KI korrekt
[ ] Fehlerbehandlung (insufficient data)
[ ] Test: Platzhalter liefert gleichen Output wie vorher
[ ] In PLACEHOLDER_MAP registriert
[ ] Dokumentiert

Erstellt: 28. März 2026 Status: Ready for Phase 0c Implementation Nächster Schritt: Data Layer Funktionen implementieren (Start mit utils.py)