feat: Update placeholder metadata and nutrition metrics
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

- 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.
This commit is contained in:
Lars 2026-04-11 21:11:05 +02:00
parent 2ea5f905c4
commit 052ba195cc
11 changed files with 159 additions and 77 deletions

View File

@ -92,16 +92,10 @@ registry = get_registry()
**Package:** `backend/placeholder_registrations/`
**Struktur:**
```
placeholder_registrations/
├── __init__.py # Auto-Import aller Registrations
├── nutrition_part_a.py # Nutrition Basis-Metriken (4 Placeholder)
├── nutrition_part_b.py # Protein-Ziele (5 Placeholder) - TODO
├── body_metrics.py # Körper-Metriken - TODO
├── activity_metrics.py # Aktivitäts-Metriken - TODO
└── ... # Weitere Cluster
```
**Struktur:** Vollständige Cluster-Module (u. a. Ernährung, Körper, Aktivität, Schlaf,
Vitalwerte, Profil/Zeitraum, Phase-0b-Ziele, Korrelationen); siehe `__init__.py` für die
Import-Liste. **Anzahl:** 114 Platzhalter, identisch zu `PLACEHOLDER_MAP` in
`placeholder_resolver.py`.
**Auto-Registration:**
- Import des Package triggert automatische Registrierung aller Placeholder

View File

@ -7,7 +7,7 @@
## Gesamt-Übersicht
**Aktuelle Platzhalter:** 116
**Aktuelle Platzhalter:** 114 (PLACEHOLDER_MAP / Registry)
**Nach Phase 0c Migration:**
- ✅ **Bleiben einfach (kein Data Layer):** 8 Platzhalter
- 🔄 **Gehen zu Data Layer:** 108 Platzhalter

View File

@ -107,7 +107,7 @@ frontend/src/
### Updates (11.04.2026 - Placeholder Phase A)
- **`main.py`:** `import placeholder_registrations` beim Start, damit die Registry (48 Keys) und `get_placeholder_catalog()` ohne vorherigen Export-Request konsistent sind.
- **`main.py`:** `import placeholder_registrations` beim Start, damit die Registry (**114 Keys**, deckungsgleich `PLACEHOLDER_MAP`) und `get_placeholder_catalog()` ohne vorherigen Export-Request konsistent sind.
- **`placeholder_resolver.py`:** `{{top_goal_progress_pct}}` nutzt `_safe_int` statt `_safe_str` (Verdrahtung zu `scores.get_top_priority_goal` korrigiert).
### Updates (11.04.2026 - Gitea #75, nutrition_score Registry)
@ -115,10 +115,11 @@ frontend/src/
- **Gitea #75** (offen): Zucker/Ballaststoffe/Lebensmittelqualität, automatisches Lebensmittelprofil, später Mahlzeiten-Timing/Abgleich mit Training — http://192.168.2.144:3000/Lars/mitai-jinkendo/issues/75
- **`nutrition_score`:** Registry in `backend/placeholder_registrations/nutrition_score.py`, Import in `placeholder_registrations/__init__.py`; Legacy-Duplikat unter „Scores“ im Platzhalter-Katalog entfernt.
### Updates (11.04.2026 - Ernährung: eine TDEE-/Tageslogik)
### Updates (11.04.2026 - Ernährung: TDEE, Bilanz, Kalorien-Score)
- **`data_layer/nutrition_metrics.py`:** TDEE für Bilanz = **aktuelles Gewicht × 32,5 kcal/kg** (`estimate_tdee_kcal_from_latest_weight`); `get_energy_balance_data` und `calculate_energy_balance_7d` nutzen **tägliche kcal-Summen** (nicht Rohzeilen). Makro-Durchschnitte über **Tagesmittel**; `protein_adequacy_28d`, `macro_consistency_score`, `get_protein_adequacy_data`, `get_macro_consistency_data` auf **Kalendertag** umgestellt. Entfernt: festes **2500 kcal** in `get_energy_balance_data`.
- **`data_layer/nutrition_metrics.py`:** TDEE für Bilanz: primär **MifflinSt Jeor BMR × PAL 1,55**, wenn Profil (Größe, Geschlecht, DOB) und Gewicht vorhanden; sonst Fallback **kg × 32,5** (`estimate_tdee_kcal_from_latest_weight`). `get_energy_balance_data` / `calculate_energy_balance_7d` nutzen **tägliche kcal-Summen**. **`_score_calorie_adherence`** (Komponente von `calculate_nutrition_score`) wertet die 7-Tage-Bilanz nach **`profiles.goal_mode`** aus (weight_loss vs. strength/recomposition vs. maintenance/health/endurance).
- **`routers/charts.py`:** `/charts/energy-balance` und Protein-Timeline nutzen dieselbe TDEE-/Tageslogik; ohne `weight_log` liefert Energiebilanz-Chart eine klare Fehlermeldung. Adherence-Endpoint: Kcal-CV über **Tages-Summen**.
- **Doku:** Normative Platzhalter-Zahl **114** (`docs/PLACEHOLDER_*.md`); `placeholder_metadata_complete.py` als **Legacy** gekennzeichnet — maßgeblich `placeholder_registrations/` + `PLACEHOLDER_REGISTRY_FRAMEWORK.md`.
### GUI / Informationsarchitektur (Abnahme dieser Iteration, 2026-04-05)

View File

@ -25,14 +25,43 @@ from datetime import datetime, timedelta, date
from db import get_db, get_cursor, r2d
from data_layer.utils import calculate_confidence, safe_float, safe_int
# Single TDEE rule for placeholders, charts, and warnings (kcal/day = kg * factor).
# Replaces legacy fixed 2500 kcal so all consumers stay aligned.
# Fallback TDEE (kcal/day) when demographics for MifflinSt Jeor are incomplete.
TDEE_KCAL_PER_KG_BODYWEIGHT = 32.5
# PAL applied to MSJ BMR when height, sex, dob and weight are available (moderate activity).
TDEE_PAL_MODERATE = 1.55
def _age_years_from_dob(dob) -> Optional[int]:
if dob is None:
return None
try:
if isinstance(dob, str):
birth = datetime.strptime(dob[:10], "%Y-%m-%d").date()
else:
birth = dob
today = date.today()
return today.year - birth.year - ((today.month, today.day) < (birth.month, birth.day))
except Exception:
return None
def _mifflin_st_jeor_bmr_kcal(
weight_kg: float, height_cm: float, age_years: int, sex_is_male: bool
) -> float:
if sex_is_male:
return 10.0 * weight_kg + 6.25 * height_cm - 5.0 * age_years + 5.0
return 10.0 * weight_kg + 6.25 * height_cm - 5.0 * age_years - 161.0
def estimate_tdee_kcal_from_latest_weight(profile_id: str) -> Optional[float]:
"""
Estimated TDEE (kcal/day) from latest body weight.
Estimated TDEE (kcal/day).
Primary: MifflinSt Jeor BMR × TDEE_PAL_MODERATE when latest weight plus
profiles.height, profiles.sex, profiles.dob are usable.
Fallback: latest weight (kg) × TDEE_KCAL_PER_KG_BODYWEIGHT (legacy heuristic).
Returns None if no weight on record.
"""
with get_db() as conn:
@ -42,10 +71,41 @@ def estimate_tdee_kcal_from_latest_weight(profile_id: str) -> Optional[float]:
WHERE profile_id=%s ORDER BY date DESC LIMIT 1""",
(profile_id,),
)
row = cur.fetchone()
if not row or row["weight"] is None:
wrow = cur.fetchone()
if not wrow or wrow["weight"] is None:
return None
return float(row["weight"]) * TDEE_KCAL_PER_KG_BODYWEIGHT
weight_kg = float(wrow["weight"])
cur.execute(
"SELECT height, sex, dob FROM profiles WHERE id=%s",
(profile_id,),
)
prow = cur.fetchone()
if prow and prow.get("height") and prow.get("sex") is not None and prow.get("dob"):
height_cm = float(prow["height"])
age = _age_years_from_dob(prow["dob"])
if age is not None and 10 < age < 120 and height_cm > 50:
sex_raw = str(prow["sex"]).strip().lower()
sex_is_male = sex_raw in ("m", "male", "männlich", "mann")
bmr = _mifflin_st_jeor_bmr_kcal(weight_kg, height_cm, age, sex_is_male)
if bmr > 400:
return bmr * TDEE_PAL_MODERATE
return weight_kg * TDEE_KCAL_PER_KG_BODYWEIGHT
def _get_profile_goal_mode(profile_id: str) -> str:
"""Strategic goal_mode from profiles (Phase 0a); defaults to health."""
with get_db() as conn:
cur = get_cursor(conn)
cur.execute("SELECT goal_mode FROM profiles WHERE id=%s", (profile_id,))
row = cur.fetchone()
if row and row.get("goal_mode"):
g = str(row["goal_mode"]).strip().lower()
if g:
return g
return "health"
def get_nutrition_average_data(
@ -224,7 +284,7 @@ def get_energy_balance_data(
Energy balance (intake - estimated expenditure), kcal/day.
Intake: mean of daily total kcal (sum per calendar day).
TDEE: latest weight (kg) * TDEE_KCAL_PER_KG_BODYWEIGHT (same rule as placeholders).
TDEE: estimate_tdee_kcal_from_latest_weight (MSJ × PAL oder kg-Fallback).
"""
with get_db() as conn:
cur = get_cursor(conn)
@ -834,32 +894,58 @@ def calculate_nutrition_score(profile_id: str, focus_weights: Optional[Dict] = N
def _score_calorie_adherence(profile_id: str) -> Optional[int]:
"""Score calorie target adherence (0-100)"""
# Check for energy balance goal
# For now, use energy balance calculation
"""Score calorie target adherence (0100) using 7d balance vs profiles.goal_mode."""
balance = calculate_energy_balance_7d(profile_id)
if balance is None:
return None
# Score based on whether deficit/surplus aligns with goal
# Simplified: assume weight loss goal = deficit is good
# TODO: Check actual goal type
mode = _get_profile_goal_mode(profile_id)
b = float(balance)
abs_balance = abs(balance)
def _weight_loss(x: float) -> int:
if -550 <= x <= -250:
return 100
if x > 450:
return 38
if -750 <= x < -550 or -250 < x <= 120:
return 82
if x < -1200:
return 52
if -950 <= x < -750 or 120 < x <= 350:
return 68
return 58
# Moderate deficit/surplus = good
if 200 <= abs_balance <= 500:
return 100
elif 100 <= abs_balance <= 700:
return 85
elif abs_balance <= 900:
return 70
elif abs_balance <= 1200:
return 55
else:
def _surplus_friendly(x: float) -> int:
if 80 <= x <= 480:
return 100
if -120 <= x < 80 or 480 < x <= 700:
return 86
if -380 <= x < -120:
return 68
if x > 850:
return 54
if x < -650:
return 44
return 72
def _maintenance(x: float) -> int:
a = abs(x)
if a <= 200:
return 100
if a <= 400:
return 84
if a <= 650:
return 70
if a <= 900:
return 55
return 40
if mode == "weight_loss":
return _weight_loss(b)
if mode in ("strength", "recomposition"):
return _surplus_friendly(b)
return _maintenance(b)
def _score_macro_balance(profile_id: str) -> Optional[int]:
"""Score macro balance (0-100)"""

View File

@ -1,11 +1,10 @@
"""
Complete Placeholder Metadata Definitions
Complete Placeholder Metadata Definitions (Legacy / Normativ v1)
This module contains manually curated, complete metadata for all 116 placeholders.
It combines automatic extraction with manual annotation to ensure 100% normative compliance.
IMPORTANT: This is the authoritative source for placeholder metadata.
All new placeholders MUST be added here with complete metadata.
Hinweis (2026-04): **Verbindliche Metadaten-Pflege** erfolgt über
`backend/placeholder_registrations/` + `placeholder_registry.py` (114 Keys, deckungsgleich
mit `PLACEHOLDER_MAP`). Dieses Modul bleibt für ältere Generator-/Export-Pfade und
Tests; neue Platzhalter hier nicht mehr duplizieren.
"""
from placeholder_metadata import (
PlaceholderMetadata,
@ -28,7 +27,7 @@ from typing import List
def get_all_placeholder_metadata() -> List[PlaceholderMetadata]:
"""
Returns complete metadata for all 116 placeholders.
Returns complete metadata for all 114 placeholders (Registry ist maßgeblich).
This is the authoritative, manually curated source.
"""
@ -476,7 +475,7 @@ def get_all_placeholder_metadata() -> List[PlaceholderMetadata]:
notes=["Quadrant-Logik basiert auf FM/LBM Delta-Vorzeichen"],
),
# NOTE: Continuing with all 116 placeholders would make this file very long.
# NOTE: Continuing with all 114 placeholders would make this file very long.
# For brevity, I'll create a separate generator that fills all remaining placeholders.
# The pattern is established above - each placeholder gets full metadata.
]

View File

@ -53,6 +53,13 @@ def register_nutrition_part_a():
"layer_1_decision": "Data Layer (nutrition_metrics.get_nutrition_average_data)",
"layer_2a_decision": "Placeholder Resolver (formatting only)",
"architecture_alignment": "Phase 0c Multi-Layer Architecture conform",
"minimum_data_requirements": (
"Mind. ein Kalendertag mit nutrition_log im Fenster; Mittelwerte aus täglicher Aggregation. "
"Confidence über calculate_confidence(day_count, days) in get_nutrition_average_data."
),
"quality_filter_policy": (
"Kein Outlier-Filter auf Tagesaggregaten; leere Tage fehlen in der Aggregation (kein Imputing)."
),
}
# Common evidence for shared fields
@ -73,8 +80,8 @@ def register_nutrition_part_a():
"layer_2b_reuse_possible": EvidenceType.TO_VERIFY, # not verified in charts
"architecture_alignment": EvidenceType.CODE_DERIVED, # imports from data_layer
"issue_53_alignment": EvidenceType.MIXED, # layer separation visible, issue conformity derived
"minimum_data_requirements": EvidenceType.UNRESOLVED, # not explicit in code
"quality_filter_policy": EvidenceType.UNRESOLVED, # not implemented
"minimum_data_requirements": EvidenceType.CODE_DERIVED,
"quality_filter_policy": EvidenceType.CODE_DERIVED,
}
# ── kcal_avg ──────────────────────────────────────────────────────────────
@ -94,8 +101,6 @@ def register_nutrition_part_a():
known_limitations="nur Intake, kein Bedarf; sagt allein nichts über Zielpassung",
layer_2b_reuse_possible=None, # to_verify - not checked in chart code
issue_53_alignment="Layer separation established",
minimum_data_requirements=None, # unresolved
quality_filter_policy=None, # unresolved
**common_metadata
)
@ -131,8 +136,6 @@ def register_nutrition_part_a():
),
layer_2b_reuse_possible=None,
issue_53_alignment="Layer separation established",
minimum_data_requirements=None,
quality_filter_policy=None,
**common_metadata
)
@ -165,8 +168,6 @@ def register_nutrition_part_a():
),
layer_2b_reuse_possible=None,
issue_53_alignment="Layer separation established",
minimum_data_requirements=None,
quality_filter_policy=None,
**common_metadata
)
@ -196,8 +197,6 @@ def register_nutrition_part_a():
known_limitations="meist im Gesamtkontext der Makroverteilung relevant",
layer_2b_reuse_possible=None,
issue_53_alignment="Layer separation established",
minimum_data_requirements=None,
quality_filter_policy=None,
**common_metadata
)

View File

@ -113,7 +113,7 @@ energy_balance_metadata = PlaceholderMetadata(
resolver_function="_safe_float('energy_balance_7d', pid, decimals=0)",
data_layer_module="backend/data_layer/nutrition_metrics.py",
data_layer_function="calculate_energy_balance_7d",
source_tables=["nutrition_log", "weight_log"],
source_tables=["nutrition_log", "weight_log", "profiles"],
# Semantic
semantic_contract="Liefert die geschätzte Energiebilanz über 7 Tage als Differenz zwischen durchschnittlicher Energieaufnahme und geschätztem TDEE (Total Daily Energy Expenditure). Positiver Wert = Überschuss, Negativer Wert = Defizit.",
@ -127,11 +127,14 @@ energy_balance_metadata = PlaceholderMetadata(
# Quality
minimum_data_requirements="Mindestens 4 Tage mit Kalorienerfassung in 7-Tage-Fenster. Aktuelles Gewicht aus weight_log erforderlich.",
quality_filter_policy="Unvollständige Intake-Daten und fehlende Gewichtsmessung reduzieren Verlässlichkeit. TDEE-Schätzung ist vereinfacht (weight_kg × 32.5).",
quality_filter_policy=(
"Unvollständige Intake-Daten und fehlende Gewichtsmessung reduzieren Verlässlichkeit. "
"TDEE: MifflinSt Jeor × PAL 1.55 wenn Höhe, Geschlecht, DOB und Gewicht vorhanden, sonst kg×32.5."
),
confidence_logic=(
"Kombiniert Intake-Abdeckung und Robustheit des Verbrauchsmodells. "
"Niedrigere Confidence bei <7 Tagen Daten oder fehlendem Gewicht. "
"TDEE-Modell ist vereinfacht → inherent uncertainty."
"PAL=1.55 ist ein Festwert (moderate Aktivität), kein individuelles Aktivitätslogging."
),
missing_value_policy=MissingValuePolicy(
available=False,
@ -140,11 +143,10 @@ energy_balance_metadata = PlaceholderMetadata(
legacy_display="nicht verfügbar"
),
known_limitations=(
"TDEE-MODELL: Vereinfacht als bodyweight_kg × 32.5 (mittlerer Multiplikator). "
"NICHT berücksichtigt: Aktivitätslevel, Alter, Geschlecht, Stoffwechselanpassungen. "
"TODO in Code: Harris-Benedict oder Mifflin-St Jeor für präzisere TDEE-Schätzung. "
"ACHTUNG: Energiebilanz ist modellbasiert, nicht direkt gemessen. "
"Einheit ist kcal/Tag (daily average), NICHT 7d-Total."
"TDEE: Bei vollständigem Profil (Größe, Geschlecht, DOB, Gewicht) MifflinSt Jeor BMR × 1.55; "
"sonst Fallback kg×32.5. PAL ist nicht nutzerkonfigurierbar. "
"Energiebilanz ist modellbasiert, nicht gemessen. "
"Einheit kcal/Tag (Tagesmittel), nicht 7-Tage-Summe."
),
# Architecture

View File

@ -60,8 +60,9 @@ nutrition_score_metadata = PlaceholderMetadata(
),
known_limitations=(
"Abhängig von user_focus_area_weights; ohne Ernährungs-Fokus liefert die "
"Funktion None. Kalorien-Adhärenz nutzt vereinfachte Heuristik (goal_type-TODO). "
"_score_macro_balance nutzt noch zeilenbasierte 28d-Abfrage (langfristig an "
"Funktion None. Kalorien-Adhärenz nutzt 7d-Energiebilanz vs. profiles.goal_mode "
"(weight_loss / strength+recomposition / sonst maintenance). "
"_score_macro_balance nutzt zeilenbasierte 28d-Abfrage (langfristig an "
"Tagesaggregation angleichen)."
),
layer_1_decision="Data Layer (nutrition_metrics.calculate_nutrition_score)",

View File

@ -18,7 +18,7 @@ This document establishes **mandatory governance rules** for placeholder managem
## 2. Scope
These guidelines apply to:
- All 116 existing placeholders
- All 114 existing placeholders (canonical: `PLACEHOLDER_MAP`)
- All new placeholders
- All modifications to existing placeholders
- All placeholder deprecations

View File

@ -79,7 +79,7 @@ curl -s -H "X-Auth-Token: $TOKEN" \
**Expected response:**
```json
{
"total_placeholders": 116,
"total_placeholders": 114,
"available": 98,
"missing": 18,
"by_type": {

View File

@ -9,12 +9,12 @@
## Executive Summary
This document summarizes the complete implementation of the normative placeholder metadata system for Mitai Jinkendo. The system provides a comprehensive, standardized framework for managing, documenting, and validating all 116 placeholders in the system.
This document summarizes the complete implementation of the normative placeholder metadata system for Mitai Jinkendo. The system provides a comprehensive, standardized framework for managing, documenting, and validating all 114 placeholders in the system.
**Key Achievements:**
- ✅ Complete metadata schema (normative compliant)
- ✅ Automatic metadata extraction
- ✅ Manual curation for 116 placeholders
- ✅ Manual curation for 114 placeholders
- ✅ Extended export API (non-breaking)
- ✅ Catalog generator (4 documentation files)
- ✅ Validation & testing framework
@ -75,7 +75,7 @@ This document summarizes the complete implementation of the normative placeholde
### 1.3 Complete Metadata Definitions
#### `backend/placeholder_metadata_complete.py` (220 lines, expandable to all 116)
#### `backend/placeholder_metadata_complete.py` (220 lines, expandable to all 114)
**Purpose:** Manually curated, authoritative metadata for all placeholders
@ -106,7 +106,7 @@ PlaceholderMetadata(
**Key Features:**
- Hand-curated for accuracy
- Complete for all 116 placeholders
- Complete for all 114 placeholders
- Serves as authoritative source
- Normative compliant
@ -285,7 +285,7 @@ pytest backend/tests/test_placeholder_metadata.py -v
v
┌─────────────────────────────────────────────────────────────┐
│ Complete Registry │
│ (116 placeholders with full metadata) │
│ (114 placeholders with full metadata) │
└──────────┬──────────────────────────────────────────────────┘
├──> Generation Scripts (generate_*.py)
@ -309,7 +309,7 @@ pytest backend/tests/test_placeholder_metadata.py -v
### 3.1 Metadata Extraction Flow
```
1. PLACEHOLDER_MAP (116 entries)
1. PLACEHOLDER_MAP (114 entries)
└─> extract_resolver_name()
└─> analyze_data_layer_usage()
└─> infer_type/time_window/output_type()
@ -468,7 +468,7 @@ curl -H "X-Auth-Token: <token>" \
# Output:
{
"total_placeholders": 116,
"total_placeholders": 114,
"available": 98,
"missing": 18,
"by_type": {
@ -599,7 +599,7 @@ The system is designed for extensibility:
## 8. Compliance Checklist
✅ **Normative Standard Compliance:**
- All 116 placeholders inventoried
- All 114 placeholders inventoried
- Complete metadata schema implemented
- Validation framework in place
- Non-breaking export API