- Added new functions for BMI and goal weight/body fat percentage retrieval in `body_metrics.py`. - Introduced training frequency and inter-session gap calculations in `activity_metrics.py`. - Updated placeholder registrations to include new metrics for nutrition and activity. - Improved data handling in `placeholder_resolver.py` for better integration of new metrics. - Enhanced documentation across modules to reflect the new functionalities. These updates improve the accuracy and comprehensiveness of health and fitness assessments within the application.
1309 lines
75 KiB
Python
1309 lines
75 KiB
Python
"""
|
||
Body Metrics Placeholder Registrations
|
||
|
||
Registers 17 Körper-Metriken in diesem Modul; insgesamt 21 Körper-Keys in der Registry
|
||
(zusätzlich body_extras.py: bmi, goal_weight, goal_bf_pct, body_progress_score).
|
||
|
||
Weight & Trends (7):
|
||
- weight_aktuell
|
||
- weight_trend
|
||
- weight_7d_median
|
||
- weight_28d_slope
|
||
- weight_90d_slope
|
||
|
||
Body Composition (5):
|
||
- kf_aktuell
|
||
- fm_28d_change
|
||
- lbm_28d_change
|
||
- waist_hip_ratio
|
||
- recomposition_quadrant
|
||
|
||
Circumference Deltas (5):
|
||
- waist_28d_delta
|
||
- arm_28d_delta
|
||
- chest_28d_delta
|
||
- hip_28d_delta
|
||
- thigh_28d_delta
|
||
|
||
Summaries (2):
|
||
- caliper_summary
|
||
- circ_summary
|
||
|
||
Evidence-based metadata with comprehensive formula documentation.
|
||
Siehe backend/data_layer/body_metrics.py als Layer-1-Implementierung.
|
||
"""
|
||
|
||
from placeholder_registry import (
|
||
PlaceholderMetadata,
|
||
MissingValuePolicy,
|
||
EvidenceType,
|
||
OutputType,
|
||
PlaceholderType,
|
||
register_placeholder
|
||
)
|
||
|
||
|
||
def register_body_metrics():
|
||
"""
|
||
Register all body metrics placeholders.
|
||
|
||
Metadata sources:
|
||
- CODE_DERIVED: extracted from code inspection
|
||
- DRAFT_DERIVED: from canonical requirements
|
||
- MIXED: combination of code + interpretation
|
||
- UNRESOLVED: not explicitly documented
|
||
- TO_VERIFY: architecture decisions pending review
|
||
"""
|
||
|
||
# ═════════════════════════════════════════════════════════════════════════
|
||
# WEIGHT & TRENDS (7 Placeholders)
|
||
# ═════════════════════════════════════════════════════════════════════════
|
||
|
||
# ── weight_aktuell ───────────────────────────────────────────────────────
|
||
|
||
weight_aktuell_metadata = PlaceholderMetadata(
|
||
key="weight_aktuell",
|
||
category="Körper",
|
||
description="Aktuelles Gewicht in kg",
|
||
|
||
# Technical
|
||
resolver_module="backend/placeholder_resolver.py",
|
||
resolver_function="get_latest_weight",
|
||
data_layer_module="backend/data_layer/body_metrics.py",
|
||
data_layer_function="get_latest_weight_data",
|
||
source_tables=["weight_log"],
|
||
|
||
# Semantic
|
||
semantic_contract=(
|
||
"Liefert den zuletzt gültigen gemessenen Gewichtswert des Nutzers in Kilogramm "
|
||
"als Snapshot. Der Wert ist kein Trend und keine Glättung, sondern der jüngste "
|
||
"verfügbare Messpunkt."
|
||
),
|
||
business_meaning="Basiswert für Körperstatus, Zielabgleich, BMI-Berechnung, Trend- und Recomposition-Kontext",
|
||
unit="kg",
|
||
time_window="latest",
|
||
output_type=OutputType.NUMERIC,
|
||
placeholder_type=PlaceholderType.RAW_DATA,
|
||
format_hint="Dezimalzahl in kg",
|
||
example_output="78.4",
|
||
|
||
# Quality
|
||
minimum_data_requirements="Mindestens 1 gültiger Gewichtseintrag",
|
||
quality_filter_policy="Offensichtlich ungültige oder technisch fehlerhafte Werte ausschließen",
|
||
confidence_logic="high wenn Eintrag vorhanden, insufficient wenn nicht",
|
||
missing_value_policy=MissingValuePolicy(
|
||
available=False,
|
||
value_raw=None,
|
||
missing_reason="no_data",
|
||
legacy_display="nicht verfügbar"
|
||
),
|
||
known_limitations=(
|
||
"Ein einzelner Messwert ist stark tagesabhängig. "
|
||
"Nicht isoliert interpretieren, wenn Trends oder Zielbewertung gefragt sind. "
|
||
"Für Veränderungsaussagen immer mit Trend-/Median-/Delta-Placeholdern kombinieren."
|
||
),
|
||
|
||
# Architecture
|
||
layer_1_decision="Data Layer (body_metrics.get_latest_weight_data)",
|
||
layer_2a_decision="Placeholder Resolver (formatting only: f'{weight:.1f} kg')",
|
||
layer_2b_reuse_possible="Ja - weight value direkt nutzbar für Charts",
|
||
architecture_alignment="Phase 0c Multi-Layer Architecture conform",
|
||
issue_53_alignment="Vollständige Layer-Trennung: Data Layer → Resolver → AI/Charts"
|
||
)
|
||
|
||
# Evidence tagging
|
||
weight_aktuell_metadata.set_evidence("category", EvidenceType.CODE_DERIVED) # placeholder_resolver.py:1367
|
||
weight_aktuell_metadata.set_evidence("resolver_module", EvidenceType.CODE_DERIVED)
|
||
weight_aktuell_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED) # PLACEHOLDER_MAP line 1083
|
||
weight_aktuell_metadata.set_evidence("data_layer_module", EvidenceType.CODE_DERIVED) # import at top
|
||
weight_aktuell_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED) # body_metrics.py:26
|
||
weight_aktuell_metadata.set_evidence("source_tables", EvidenceType.CODE_DERIVED) # SQL query line 49
|
||
weight_aktuell_metadata.set_evidence("semantic_contract", EvidenceType.DRAFT_DERIVED) # canonical line 808
|
||
weight_aktuell_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED) # canonical line 809
|
||
weight_aktuell_metadata.set_evidence("unit", EvidenceType.CODE_DERIVED) # resolver return "kg"
|
||
weight_aktuell_metadata.set_evidence("time_window", EvidenceType.CODE_DERIVED) # ORDER BY date DESC LIMIT 1
|
||
weight_aktuell_metadata.set_evidence("output_type", EvidenceType.CODE_DERIVED) # return type numeric
|
||
weight_aktuell_metadata.set_evidence("placeholder_type", EvidenceType.MIXED) # raw data, no interpretation
|
||
weight_aktuell_metadata.set_evidence("format_hint", EvidenceType.CODE_DERIVED) # f-string formatting
|
||
weight_aktuell_metadata.set_evidence("example_output", EvidenceType.DRAFT_DERIVED) # canonical line 814
|
||
weight_aktuell_metadata.set_evidence("minimum_data_requirements", EvidenceType.DRAFT_DERIVED)
|
||
weight_aktuell_metadata.set_evidence("quality_filter_policy", EvidenceType.DRAFT_DERIVED)
|
||
weight_aktuell_metadata.set_evidence("confidence_logic", EvidenceType.CODE_DERIVED) # body_metrics.py:58-67
|
||
weight_aktuell_metadata.set_evidence("missing_value_policy", EvidenceType.CODE_DERIVED) # resolver line 64
|
||
weight_aktuell_metadata.set_evidence("known_limitations", EvidenceType.DRAFT_DERIVED) # canonical line 837-840
|
||
weight_aktuell_metadata.set_evidence("layer_1_decision", EvidenceType.CODE_DERIVED)
|
||
weight_aktuell_metadata.set_evidence("layer_2a_decision", EvidenceType.CODE_DERIVED)
|
||
weight_aktuell_metadata.set_evidence("layer_2b_reuse_possible", EvidenceType.TO_VERIFY)
|
||
weight_aktuell_metadata.set_evidence("architecture_alignment", EvidenceType.CODE_DERIVED)
|
||
weight_aktuell_metadata.set_evidence("issue_53_alignment", EvidenceType.MIXED)
|
||
|
||
register_placeholder(weight_aktuell_metadata)
|
||
|
||
# ── weight_trend ─────────────────────────────────────────────────────────
|
||
|
||
weight_trend_metadata = PlaceholderMetadata(
|
||
key="weight_trend",
|
||
category="Körper",
|
||
description="Gewichtstrend (28d)",
|
||
|
||
# Technical
|
||
resolver_module="backend/placeholder_resolver.py",
|
||
resolver_function="get_weight_trend",
|
||
data_layer_module="backend/data_layer/body_metrics.py",
|
||
data_layer_function="get_weight_trend_data",
|
||
source_tables=["weight_log"],
|
||
|
||
# Semantic
|
||
semantic_contract=(
|
||
"Liefert den Gewichtstrend über ein 28-Tage-Fenster auf Basis einer einfachen "
|
||
"Delta-Berechnung (erster vs. letzter Wert). Der Placeholder beschreibt Richtung "
|
||
"und Veränderungstendenz des Gewichts über dieses Fenster."
|
||
),
|
||
business_meaning="Verdichteter Gewichtsverlauf für Fortschritts- und Diagnoseaussagen",
|
||
unit="kg over 28d",
|
||
time_window="28d",
|
||
output_type=OutputType.STRING,
|
||
placeholder_type=PlaceholderType.INTERPRETED,
|
||
format_hint="String: 'steigend (+2.3 kg in 28 Tagen)' | 'sinkend (-1.8 kg)' | 'stabil'",
|
||
example_output="sinkend (-1.8 kg in 28 Tagen)",
|
||
|
||
# Quality
|
||
minimum_data_requirements=(
|
||
"Minimum 8 valide Messpunkte (low confidence), "
|
||
"sinnvoll 12+ (medium), stark belastbar 18+ (high)"
|
||
),
|
||
quality_filter_policy="Ausreißer, Duplikate und technisch unplausible Werte sollen Trend nicht verzerren",
|
||
confidence_logic=(
|
||
"high: >= 18 points (28d), "
|
||
"medium: >= 12 points, "
|
||
"low: >= 8 points, "
|
||
"insufficient: < 8 points"
|
||
),
|
||
missing_value_policy=MissingValuePolicy(
|
||
available=False,
|
||
value_raw=None,
|
||
missing_reason="insufficient_data",
|
||
legacy_display="nicht genug Daten"
|
||
),
|
||
known_limitations=(
|
||
"ZEIT-INKONSISTENZ: Funktion akzeptiert days-Parameter (default 28d), "
|
||
"aber PLACEHOLDER_MAP nutzt fixen default. "
|
||
"Canonical fordert festes 28d-Fenster. "
|
||
"STABILITÄT: abs(delta) < 0.3 kg → 'stabil' (hardcoded threshold). "
|
||
"Delta-basiert, keine lineare Regression (siehe weight_28d_slope für Slope)."
|
||
),
|
||
|
||
# Architecture
|
||
layer_1_decision="Data Layer (body_metrics.get_weight_trend_data) - berechnet delta + direction",
|
||
layer_2a_decision="Placeholder Resolver (formatting: steigend/sinkend/stabil mit delta)",
|
||
layer_2b_reuse_possible="Teilweise - delta + direction nutzbar, formatting Chart-spezifisch",
|
||
architecture_alignment="Phase 0c conform",
|
||
issue_53_alignment="Layer-Trennung etabliert, aber Zeit-Parametrisierung unklar"
|
||
)
|
||
|
||
# Evidence tagging
|
||
weight_trend_metadata.set_evidence("category", EvidenceType.CODE_DERIVED)
|
||
weight_trend_metadata.set_evidence("resolver_module", EvidenceType.CODE_DERIVED)
|
||
weight_trend_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED) # line 1084
|
||
weight_trend_metadata.set_evidence("data_layer_module", EvidenceType.CODE_DERIVED)
|
||
weight_trend_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED) # body_metrics.py:71
|
||
weight_trend_metadata.set_evidence("source_tables", EvidenceType.CODE_DERIVED) # SQL line 109
|
||
weight_trend_metadata.set_evidence("semantic_contract", EvidenceType.MIXED) # draft + code shows delta calc
|
||
weight_trend_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED)
|
||
weight_trend_metadata.set_evidence("unit", EvidenceType.MIXED) # kg change over period
|
||
weight_trend_metadata.set_evidence("time_window", EvidenceType.MIXED) # canonical says 28d, code accepts parameter
|
||
weight_trend_metadata.set_evidence("output_type", EvidenceType.CODE_DERIVED) # returns string
|
||
weight_trend_metadata.set_evidence("placeholder_type", EvidenceType.MIXED)
|
||
weight_trend_metadata.set_evidence("format_hint", EvidenceType.CODE_DERIVED) # resolver lines 84-89
|
||
weight_trend_metadata.set_evidence("example_output", EvidenceType.CODE_DERIVED)
|
||
weight_trend_metadata.set_evidence("minimum_data_requirements", EvidenceType.CODE_DERIVED) # calculate_confidence
|
||
weight_trend_metadata.set_evidence("quality_filter_policy", EvidenceType.DRAFT_DERIVED)
|
||
weight_trend_metadata.set_evidence("confidence_logic", EvidenceType.CODE_DERIVED) # body_metrics.py:117
|
||
weight_trend_metadata.set_evidence("missing_value_policy", EvidenceType.CODE_DERIVED) # resolver line 79
|
||
weight_trend_metadata.set_evidence("known_limitations", EvidenceType.MIXED) # Zeit-Inkonsistenz + stability threshold
|
||
weight_trend_metadata.set_evidence("layer_1_decision", EvidenceType.CODE_DERIVED)
|
||
weight_trend_metadata.set_evidence("layer_2a_decision", EvidenceType.CODE_DERIVED)
|
||
weight_trend_metadata.set_evidence("layer_2b_reuse_possible", EvidenceType.TO_VERIFY)
|
||
weight_trend_metadata.set_evidence("architecture_alignment", EvidenceType.CODE_DERIVED)
|
||
weight_trend_metadata.set_evidence("issue_53_alignment", EvidenceType.MIXED)
|
||
|
||
register_placeholder(weight_trend_metadata)
|
||
|
||
# ── weight_7d_median ─────────────────────────────────────────────────────
|
||
|
||
weight_7d_median_metadata = PlaceholderMetadata(
|
||
key="weight_7d_median",
|
||
category="Körper",
|
||
description="Gewicht 7d Median (kg)",
|
||
|
||
# Technical
|
||
resolver_module="backend/placeholder_resolver.py",
|
||
resolver_function="_safe_float('weight_7d_median')",
|
||
data_layer_module="backend/data_layer/body_metrics.py",
|
||
data_layer_function="calculate_weight_7d_median",
|
||
source_tables=["weight_log"],
|
||
|
||
# Semantic
|
||
semantic_contract=(
|
||
"Liefert den Median der validen Gewichtsmessungen der letzten 7 Tage in Kilogramm. "
|
||
"Der Wert dient als robuster Kurzfrist-Referenzwert und ist stabiler als ein Einzelmesspunkt."
|
||
),
|
||
business_meaning="Kurzfristig geglätteter Gewichtsstatus für Status- und Verlaufsreports",
|
||
unit="kg",
|
||
time_window="7d",
|
||
output_type=OutputType.NUMERIC,
|
||
placeholder_type=PlaceholderType.INTERPRETED,
|
||
format_hint="Dezimalzahl in kg (1 Dezimalstelle)",
|
||
example_output="78.1",
|
||
|
||
# Quality
|
||
minimum_data_requirements="Minimum 4 valide Messungen (technical minimum 3), sinnvoll 5+",
|
||
quality_filter_policy="Offensichtliche Ausreißer und technisch unplausible Werte ausschließen",
|
||
confidence_logic=(
|
||
"high: >= 5 valide Messungen, "
|
||
"medium: 4 valide Messungen, "
|
||
"low: 3 valide Messungen, "
|
||
"insufficient: < 3 valide Messungen"
|
||
),
|
||
missing_value_policy=MissingValuePolicy(
|
||
available=False,
|
||
value_raw=None,
|
||
missing_reason="insufficient_data",
|
||
legacy_display="nicht verfügbar"
|
||
),
|
||
known_limitations=(
|
||
"MEDIAN-BERECHNUNG: statistics.median() - robust gegen Ausreißer. "
|
||
"ROUNDING: 1 Dezimalstelle (round(median, 1)). "
|
||
"Nur Kurzfristsicht, kein echter Trend. "
|
||
"Bei sehr geringer Messdichte wenig belastbar."
|
||
),
|
||
|
||
# Architecture
|
||
layer_1_decision="Data Layer (body_metrics.calculate_weight_7d_median)",
|
||
layer_2a_decision="Placeholder Resolver (_safe_float wrapper für formatting)",
|
||
layer_2b_reuse_possible="Ja - Median-Wert direkt nutzbar",
|
||
architecture_alignment="Phase 0c conform",
|
||
issue_53_alignment="Layer-Trennung vollständig"
|
||
)
|
||
|
||
# Evidence tagging
|
||
weight_7d_median_metadata.set_evidence("category", EvidenceType.CODE_DERIVED)
|
||
weight_7d_median_metadata.set_evidence("resolver_module", EvidenceType.CODE_DERIVED)
|
||
weight_7d_median_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED) # _safe_float line 486
|
||
weight_7d_median_metadata.set_evidence("data_layer_module", EvidenceType.CODE_DERIVED)
|
||
weight_7d_median_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED) # body_metrics.py:328
|
||
weight_7d_median_metadata.set_evidence("source_tables", EvidenceType.CODE_DERIVED) # SQL line 332
|
||
weight_7d_median_metadata.set_evidence("semantic_contract", EvidenceType.DRAFT_DERIVED)
|
||
weight_7d_median_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED)
|
||
weight_7d_median_metadata.set_evidence("unit", EvidenceType.CODE_DERIVED)
|
||
weight_7d_median_metadata.set_evidence("time_window", EvidenceType.CODE_DERIVED) # INTERVAL '7 days'
|
||
weight_7d_median_metadata.set_evidence("output_type", EvidenceType.CODE_DERIVED)
|
||
weight_7d_median_metadata.set_evidence("placeholder_type", EvidenceType.MIXED)
|
||
weight_7d_median_metadata.set_evidence("format_hint", EvidenceType.CODE_DERIVED) # round(median, 1)
|
||
weight_7d_median_metadata.set_evidence("example_output", EvidenceType.DRAFT_DERIVED)
|
||
weight_7d_median_metadata.set_evidence("minimum_data_requirements", EvidenceType.CODE_DERIVED) # len(weights) < 4
|
||
weight_7d_median_metadata.set_evidence("quality_filter_policy", EvidenceType.DRAFT_DERIVED)
|
||
weight_7d_median_metadata.set_evidence("confidence_logic", EvidenceType.DRAFT_DERIVED) # from canonical
|
||
weight_7d_median_metadata.set_evidence("missing_value_policy", EvidenceType.CODE_DERIVED) # _safe_float returns "nicht verfügbar"
|
||
weight_7d_median_metadata.set_evidence("known_limitations", EvidenceType.MIXED)
|
||
weight_7d_median_metadata.set_evidence("layer_1_decision", EvidenceType.CODE_DERIVED)
|
||
weight_7d_median_metadata.set_evidence("layer_2a_decision", EvidenceType.CODE_DERIVED)
|
||
weight_7d_median_metadata.set_evidence("layer_2b_reuse_possible", EvidenceType.TO_VERIFY)
|
||
weight_7d_median_metadata.set_evidence("architecture_alignment", EvidenceType.CODE_DERIVED)
|
||
weight_7d_median_metadata.set_evidence("issue_53_alignment", EvidenceType.CODE_DERIVED)
|
||
|
||
register_placeholder(weight_7d_median_metadata)
|
||
|
||
# ── weight_28d_slope ─────────────────────────────────────────────────────
|
||
|
||
weight_28d_slope_metadata = PlaceholderMetadata(
|
||
key="weight_28d_slope",
|
||
category="Körper",
|
||
description="Gewichtstrend 28d (kg/Tag)",
|
||
|
||
# Technical
|
||
resolver_module="backend/placeholder_resolver.py",
|
||
resolver_function="_safe_float('weight_28d_slope', decimals=4)",
|
||
data_layer_module="backend/data_layer/body_metrics.py",
|
||
data_layer_function="calculate_weight_28d_slope",
|
||
source_tables=["weight_log"],
|
||
|
||
# Semantic
|
||
semantic_contract=(
|
||
"Liefert die Steigung der linearen Regression der Gewichtswerte über 28 Tage. "
|
||
"Der Wert beschreibt die durchschnittliche tägliche Änderungsrate des Gewichts (kg/Tag)."
|
||
),
|
||
business_meaning="Feiner quantitativer Verlaufsindikator für Gewichtsentwicklung",
|
||
unit="kg/day",
|
||
time_window="28d",
|
||
output_type=OutputType.NUMERIC,
|
||
placeholder_type=PlaceholderType.INTERPRETED,
|
||
format_hint="Dezimalzahl in kg/day (4 Dezimalstellen)",
|
||
example_output="-0.026",
|
||
|
||
# Quality
|
||
minimum_data_requirements="Minimum 18 valide Messungen (60% coverage), sinnvoll 20+",
|
||
quality_filter_policy="Unplausible Ausreißer und Dubletten bereinigen",
|
||
confidence_logic=(
|
||
"high: hohe Fensterabdeckung (>=18 von 28 Tagen), "
|
||
"medium: mittlere Abdeckung, "
|
||
"low: knapp nutzbar, "
|
||
"insufficient: < 18 valide Messungen"
|
||
),
|
||
missing_value_policy=MissingValuePolicy(
|
||
available=False,
|
||
value_raw=None,
|
||
missing_reason="insufficient_data",
|
||
legacy_display="nicht verfügbar"
|
||
),
|
||
known_limitations=(
|
||
"LINEAR REGRESSION FORMEL: slope = Σ((x - x̄)(y - ȳ)) / Σ((x - x̄)²). "
|
||
"x = days since start, y = weight values. "
|
||
"MINIMUM DATA: max(18, int(28 * 0.6)) → 60% coverage required. "
|
||
"KEINE AUSREISSER-BEHANDLUNG: Einfache lineare Regression ohne Robust-Methoden. "
|
||
"ANNAHMEN: Linearer Trend, keine Beschleunigung. "
|
||
"ROUNDING: 4 Dezimalstellen (round(slope, 4)). "
|
||
"Sensibel gegenüber Messfrequenz und einzelnen extremen Messungen."
|
||
),
|
||
|
||
# Architecture
|
||
layer_1_decision="Data Layer (body_metrics.calculate_weight_28d_slope → _calculate_weight_slope)",
|
||
layer_2a_decision="Placeholder Resolver (_safe_float für formatting, 4 decimals)",
|
||
layer_2b_reuse_possible="Ja - Slope-Wert direkt nutzbar für Trend-Charts",
|
||
architecture_alignment="Phase 0c conform",
|
||
issue_53_alignment="Vollständige Layer-Trennung, Berechnung in Data Layer"
|
||
)
|
||
|
||
# Evidence tagging
|
||
weight_28d_slope_metadata.set_evidence("category", EvidenceType.CODE_DERIVED)
|
||
weight_28d_slope_metadata.set_evidence("resolver_module", EvidenceType.CODE_DERIVED)
|
||
weight_28d_slope_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED) # line 488
|
||
weight_28d_slope_metadata.set_evidence("data_layer_module", EvidenceType.CODE_DERIVED)
|
||
weight_28d_slope_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED) # body_metrics.py:348
|
||
weight_28d_slope_metadata.set_evidence("source_tables", EvidenceType.CODE_DERIVED) # _calculate_weight_slope SQL
|
||
weight_28d_slope_metadata.set_evidence("semantic_contract", EvidenceType.DRAFT_DERIVED)
|
||
weight_28d_slope_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED)
|
||
weight_28d_slope_metadata.set_evidence("unit", EvidenceType.CODE_DERIVED) # return kg/day
|
||
weight_28d_slope_metadata.set_evidence("time_window", EvidenceType.CODE_DERIVED) # days=28
|
||
weight_28d_slope_metadata.set_evidence("output_type", EvidenceType.CODE_DERIVED)
|
||
weight_28d_slope_metadata.set_evidence("placeholder_type", EvidenceType.MIXED)
|
||
weight_28d_slope_metadata.set_evidence("format_hint", EvidenceType.CODE_DERIVED) # round(slope, 4)
|
||
weight_28d_slope_metadata.set_evidence("example_output", EvidenceType.DRAFT_DERIVED)
|
||
weight_28d_slope_metadata.set_evidence("minimum_data_requirements", EvidenceType.CODE_DERIVED) # max(18, int(days * 0.6))
|
||
weight_28d_slope_metadata.set_evidence("quality_filter_policy", EvidenceType.DRAFT_DERIVED)
|
||
weight_28d_slope_metadata.set_evidence("confidence_logic", EvidenceType.MIXED) # derived from min_points
|
||
weight_28d_slope_metadata.set_evidence("missing_value_policy", EvidenceType.CODE_DERIVED)
|
||
weight_28d_slope_metadata.set_evidence("known_limitations", EvidenceType.CODE_DERIVED) # formula from body_metrics.py:358-397
|
||
weight_28d_slope_metadata.set_evidence("layer_1_decision", EvidenceType.CODE_DERIVED)
|
||
weight_28d_slope_metadata.set_evidence("layer_2a_decision", EvidenceType.CODE_DERIVED)
|
||
weight_28d_slope_metadata.set_evidence("layer_2b_reuse_possible", EvidenceType.TO_VERIFY)
|
||
weight_28d_slope_metadata.set_evidence("architecture_alignment", EvidenceType.CODE_DERIVED)
|
||
weight_28d_slope_metadata.set_evidence("issue_53_alignment", EvidenceType.CODE_DERIVED)
|
||
|
||
register_placeholder(weight_28d_slope_metadata)
|
||
|
||
# ── weight_90d_slope ─────────────────────────────────────────────────────
|
||
|
||
weight_90d_slope_metadata = PlaceholderMetadata(
|
||
key="weight_90d_slope",
|
||
category="Körper",
|
||
description="Gewichtstrend 90d (kg/Tag)",
|
||
|
||
# Technical
|
||
resolver_module="backend/placeholder_resolver.py",
|
||
resolver_function="_safe_float('weight_90d_slope', decimals=4)",
|
||
data_layer_module="backend/data_layer/body_metrics.py",
|
||
data_layer_function="calculate_weight_90d_slope",
|
||
source_tables=["weight_log"],
|
||
|
||
# Semantic
|
||
semantic_contract=(
|
||
"Liefert die Steigung der linearen Regression der Gewichtswerte über 90 Tage. "
|
||
"Der Wert dient der langfristigen Einordnung und glättet kurzfristige Schwankungen stark."
|
||
),
|
||
business_meaning="Langfristiger Verlaufsindikator für nachhaltige Gewichtsveränderung",
|
||
unit="kg/day",
|
||
time_window="90d",
|
||
output_type=OutputType.NUMERIC,
|
||
placeholder_type=PlaceholderType.INTERPRETED,
|
||
format_hint="Dezimalzahl in kg/day (4 Dezimalstellen)",
|
||
example_output="-0.011",
|
||
|
||
# Quality
|
||
minimum_data_requirements="Minimum 54 valide Messungen (60% coverage), sinnvoll 63+",
|
||
quality_filter_policy="Ausreißer und technische Fehler filtern; lange Messlücken berücksichtigen",
|
||
confidence_logic=(
|
||
"high: hohe 90d-Abdeckung (>=54 Tage), "
|
||
"medium: mittlere Abdeckung, "
|
||
"low: knapp nutzbar, "
|
||
"insufficient: < 54 valide Messungen"
|
||
),
|
||
missing_value_policy=MissingValuePolicy(
|
||
available=False,
|
||
value_raw=None,
|
||
missing_reason="insufficient_data",
|
||
legacy_display="nicht verfügbar"
|
||
),
|
||
known_limitations=(
|
||
"LINEAR REGRESSION: Gleiche Formel wie weight_28d_slope (siehe dort). "
|
||
"MINIMUM DATA: max(18, int(90 * 0.6)) = 54 Messpunkte. "
|
||
"LANGFRIST-CHARAKTERISTIK: Reagiert langsam auf neue Änderungen. "
|
||
"NICHT für kurzfristige Interventionen geeignet. "
|
||
"Saisonale Veränderungen oder Gewichtsphasenwechsel können Slope verzerren."
|
||
),
|
||
|
||
# Architecture
|
||
layer_1_decision="Data Layer (body_metrics.calculate_weight_90d_slope → _calculate_weight_slope)",
|
||
layer_2a_decision="Placeholder Resolver (_safe_float für formatting)",
|
||
layer_2b_reuse_possible="Ja - Slope-Wert nutzbar für Langzeit-Trend-Charts",
|
||
architecture_alignment="Phase 0c conform",
|
||
issue_53_alignment="Vollständige Layer-Trennung"
|
||
)
|
||
|
||
# Evidence tagging (same as weight_28d_slope, just different time window)
|
||
weight_90d_slope_metadata.set_evidence("category", EvidenceType.CODE_DERIVED)
|
||
weight_90d_slope_metadata.set_evidence("resolver_module", EvidenceType.CODE_DERIVED)
|
||
weight_90d_slope_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED) # line 489
|
||
weight_90d_slope_metadata.set_evidence("data_layer_module", EvidenceType.CODE_DERIVED)
|
||
weight_90d_slope_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED) # body_metrics.py:353
|
||
weight_90d_slope_metadata.set_evidence("source_tables", EvidenceType.CODE_DERIVED)
|
||
weight_90d_slope_metadata.set_evidence("semantic_contract", EvidenceType.DRAFT_DERIVED)
|
||
weight_90d_slope_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED)
|
||
weight_90d_slope_metadata.set_evidence("unit", EvidenceType.CODE_DERIVED)
|
||
weight_90d_slope_metadata.set_evidence("time_window", EvidenceType.CODE_DERIVED) # days=90
|
||
weight_90d_slope_metadata.set_evidence("output_type", EvidenceType.CODE_DERIVED)
|
||
weight_90d_slope_metadata.set_evidence("placeholder_type", EvidenceType.MIXED)
|
||
weight_90d_slope_metadata.set_evidence("format_hint", EvidenceType.CODE_DERIVED)
|
||
weight_90d_slope_metadata.set_evidence("example_output", EvidenceType.DRAFT_DERIVED)
|
||
weight_90d_slope_metadata.set_evidence("minimum_data_requirements", EvidenceType.CODE_DERIVED) # 60% of 90d
|
||
weight_90d_slope_metadata.set_evidence("quality_filter_policy", EvidenceType.DRAFT_DERIVED)
|
||
weight_90d_slope_metadata.set_evidence("confidence_logic", EvidenceType.MIXED)
|
||
weight_90d_slope_metadata.set_evidence("missing_value_policy", EvidenceType.CODE_DERIVED)
|
||
weight_90d_slope_metadata.set_evidence("known_limitations", EvidenceType.MIXED)
|
||
weight_90d_slope_metadata.set_evidence("layer_1_decision", EvidenceType.CODE_DERIVED)
|
||
weight_90d_slope_metadata.set_evidence("layer_2a_decision", EvidenceType.CODE_DERIVED)
|
||
weight_90d_slope_metadata.set_evidence("layer_2b_reuse_possible", EvidenceType.TO_VERIFY)
|
||
weight_90d_slope_metadata.set_evidence("architecture_alignment", EvidenceType.CODE_DERIVED)
|
||
weight_90d_slope_metadata.set_evidence("issue_53_alignment", EvidenceType.CODE_DERIVED)
|
||
|
||
register_placeholder(weight_90d_slope_metadata)
|
||
|
||
|
||
# ═════════════════════════════════════════════════════════════════════════
|
||
# BODY COMPOSITION (5 Placeholders)
|
||
# ═════════════════════════════════════════════════════════════════════════
|
||
|
||
# ── kf_aktuell ───────────────────────────────────────────────────────────
|
||
|
||
kf_aktuell_metadata = PlaceholderMetadata(
|
||
key="kf_aktuell",
|
||
category="Körper",
|
||
description="Aktueller Körperfettanteil in %",
|
||
|
||
# Technical
|
||
resolver_module="backend/placeholder_resolver.py",
|
||
resolver_function="get_latest_bf",
|
||
data_layer_module="backend/data_layer/body_metrics.py",
|
||
data_layer_function="get_body_composition_data",
|
||
source_tables=["caliper_log"],
|
||
|
||
# Semantic
|
||
semantic_contract=(
|
||
"Liefert den aktuellsten verfügbaren Körperfettwert in Prozent. "
|
||
"Der Wert ist nur im Kontext der zugrunde liegenden Messmethode interpretierbar "
|
||
"(Jackson-Pollock, Durnin-Womersley, etc.) und darf nicht als hochpräziser Laborwert "
|
||
"dargestellt werden."
|
||
),
|
||
business_meaning="Snapshot für Körperkomposition, Fortschrittsbewertung und Zielabgleich",
|
||
unit="%",
|
||
time_window="latest",
|
||
output_type=OutputType.NUMERIC,
|
||
placeholder_type=PlaceholderType.RAW_DATA,
|
||
format_hint="Prozentwert (1 Dezimalstelle)",
|
||
example_output="18.7",
|
||
|
||
# Quality
|
||
minimum_data_requirements="Mindestens 1 valider Körperfettmesspunkt (innerhalb 90d lookback)",
|
||
quality_filter_policy="Ungültige, methodisch inkonsistente oder technisch fehlerhafte Werte ausschließen",
|
||
confidence_logic=(
|
||
"high: aktueller valider Messpunkt vorhanden und Methode klar definiert, "
|
||
"medium: Messpunkt vorhanden, aber Messmethode begrenzt dokumentiert, "
|
||
"insufficient: kein valider Wert vorhanden"
|
||
),
|
||
missing_value_policy=MissingValuePolicy(
|
||
available=False,
|
||
value_raw=None,
|
||
missing_reason="no_data",
|
||
legacy_display="nicht verfügbar"
|
||
),
|
||
known_limitations=(
|
||
"MESSMETHODEN-SENSITIVITÄT: Körperfettwerte sind abhängig von Caliper-Methode "
|
||
"(sf_method: jackson_pollock, durnin_womersley, etc.). "
|
||
"LOOKBACK: 90 Tage default (get_body_composition_data(days=90)). "
|
||
"EINZELWERT-LIMITATION: Ohne Verlauf nur eingeschränkt aussagekräftig. "
|
||
"Möglichst zusammen mit anderen Körperkompositions-Placeholdern nutzen (fm_28d_change, lbm_28d_change)."
|
||
),
|
||
|
||
# Architecture
|
||
layer_1_decision="Data Layer (body_metrics.get_body_composition_data)",
|
||
layer_2a_decision="Placeholder Resolver (formatting: f'{body_fat_pct:.1f}%')",
|
||
layer_2b_reuse_possible="Ja - body_fat_pct direkt nutzbar",
|
||
architecture_alignment="Phase 0c conform",
|
||
issue_53_alignment="Layer-Trennung vollständig"
|
||
)
|
||
|
||
# Evidence tagging
|
||
kf_aktuell_metadata.set_evidence("category", EvidenceType.CODE_DERIVED)
|
||
kf_aktuell_metadata.set_evidence("resolver_module", EvidenceType.CODE_DERIVED)
|
||
kf_aktuell_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED) # line 1085
|
||
kf_aktuell_metadata.set_evidence("data_layer_module", EvidenceType.CODE_DERIVED)
|
||
kf_aktuell_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED) # body_metrics.py:159
|
||
kf_aktuell_metadata.set_evidence("source_tables", EvidenceType.CODE_DERIVED) # SQL line 188
|
||
kf_aktuell_metadata.set_evidence("semantic_contract", EvidenceType.DRAFT_DERIVED)
|
||
kf_aktuell_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED)
|
||
kf_aktuell_metadata.set_evidence("unit", EvidenceType.CODE_DERIVED) # returns %
|
||
kf_aktuell_metadata.set_evidence("time_window", EvidenceType.CODE_DERIVED) # ORDER BY date DESC LIMIT 1
|
||
kf_aktuell_metadata.set_evidence("output_type", EvidenceType.CODE_DERIVED)
|
||
kf_aktuell_metadata.set_evidence("placeholder_type", EvidenceType.MIXED)
|
||
kf_aktuell_metadata.set_evidence("format_hint", EvidenceType.CODE_DERIVED) # f-string with .1f
|
||
kf_aktuell_metadata.set_evidence("example_output", EvidenceType.DRAFT_DERIVED)
|
||
kf_aktuell_metadata.set_evidence("minimum_data_requirements", EvidenceType.MIXED)
|
||
kf_aktuell_metadata.set_evidence("quality_filter_policy", EvidenceType.DRAFT_DERIVED)
|
||
kf_aktuell_metadata.set_evidence("confidence_logic", EvidenceType.MIXED)
|
||
kf_aktuell_metadata.set_evidence("missing_value_policy", EvidenceType.CODE_DERIVED) # resolver line 102
|
||
kf_aktuell_metadata.set_evidence("known_limitations", EvidenceType.MIXED)
|
||
kf_aktuell_metadata.set_evidence("layer_1_decision", EvidenceType.CODE_DERIVED)
|
||
kf_aktuell_metadata.set_evidence("layer_2a_decision", EvidenceType.CODE_DERIVED)
|
||
kf_aktuell_metadata.set_evidence("layer_2b_reuse_possible", EvidenceType.TO_VERIFY)
|
||
kf_aktuell_metadata.set_evidence("architecture_alignment", EvidenceType.CODE_DERIVED)
|
||
kf_aktuell_metadata.set_evidence("issue_53_alignment", EvidenceType.CODE_DERIVED)
|
||
|
||
register_placeholder(kf_aktuell_metadata)
|
||
|
||
# ── fm_28d_change ────────────────────────────────────────────────────────
|
||
|
||
fm_28d_change_metadata = PlaceholderMetadata(
|
||
key="fm_28d_change",
|
||
category="Körper",
|
||
description="Fettmasse Änderung 28d (kg)",
|
||
|
||
# Technical
|
||
resolver_module="backend/placeholder_resolver.py",
|
||
resolver_function="_safe_float('fm_28d_change', decimals=2)",
|
||
data_layer_module="backend/data_layer/body_metrics.py",
|
||
data_layer_function="calculate_fm_28d_change",
|
||
source_tables=["weight_log", "caliper_log"],
|
||
|
||
# Semantic
|
||
semantic_contract=(
|
||
"Liefert die Veränderung der Fettmasse in Kilogramm über 28 Tage. "
|
||
"Negative Werte bedeuten Reduktion der Fettmasse, positive Werte Zunahme."
|
||
),
|
||
business_meaning="Kernindikator für Fettabbau bzw. Fettzunahme",
|
||
unit="kg",
|
||
time_window="28d",
|
||
output_type=OutputType.NUMERIC,
|
||
placeholder_type=PlaceholderType.INTERPRETED,
|
||
format_hint="Delta in kg (2 Dezimalstellen)",
|
||
example_output="-1.4",
|
||
|
||
# Quality
|
||
minimum_data_requirements=(
|
||
"Mindestens 2 Einträge mit SOWOHL weight ALS AUCH body_fat_pct am SELBEN Datum. "
|
||
"Sinnvoll: wiederholte Messungen mit ausreichender Abdeckung im 28d-Fenster."
|
||
),
|
||
quality_filter_policy="Methodenwechsel, inkonsistente Messserien oder unplausible BF-Werte berücksichtigen",
|
||
confidence_logic=(
|
||
"high: wiederholte konsistente Messungen, "
|
||
"medium: ausreichende aber nicht dichte Messung, "
|
||
"low: knapper Datenbasis, "
|
||
"insufficient: < 2 brauchbare Vergleichspunkte"
|
||
),
|
||
missing_value_policy=MissingValuePolicy(
|
||
available=False,
|
||
value_raw=None,
|
||
missing_reason="insufficient_data",
|
||
legacy_display="nicht verfügbar"
|
||
),
|
||
known_limitations=(
|
||
"FM-BERECHNUNG: FM = weight_kg × (body_fat_pct / 100). "
|
||
"DELTA: recent_fm - oldest_fm (most recent vs oldest in 28d window). "
|
||
"SAME-DATE REQUIREMENT: Erfordert weight + caliper Messungen am SELBEN Tag (LEFT JOIN). "
|
||
"MESSMETHODEN-ABHÄNGIGKEIT: Body Fat % ist methodensensitiv. "
|
||
"Methodenwechsel (z.B. Jackson-Pollock → Durnin-Womersley) verzerrt FM-Deltas. "
|
||
"ROUNDING: 2 Dezimalstellen (round(change, 2)). "
|
||
"BF-Messmethodenwechsel, stark schwankender BF-Wert, fehlender korrespondierender Gewichtswert sind Edge Cases."
|
||
),
|
||
|
||
# Architecture
|
||
layer_1_decision="Data Layer (body_metrics.calculate_fm_28d_change → _calculate_body_composition_change)",
|
||
layer_2a_decision="Placeholder Resolver (_safe_float für formatting)",
|
||
layer_2b_reuse_possible="Ja - FM Delta direkt nutzbar für Composition-Charts",
|
||
architecture_alignment="Phase 0c conform",
|
||
issue_53_alignment="Layer-Trennung vollständig"
|
||
)
|
||
|
||
# Evidence tagging
|
||
fm_28d_change_metadata.set_evidence("category", EvidenceType.CODE_DERIVED)
|
||
fm_28d_change_metadata.set_evidence("resolver_module", EvidenceType.CODE_DERIVED)
|
||
fm_28d_change_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED) # line 489
|
||
fm_28d_change_metadata.set_evidence("data_layer_module", EvidenceType.CODE_DERIVED)
|
||
fm_28d_change_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED) # body_metrics.py:443
|
||
fm_28d_change_metadata.set_evidence("source_tables", EvidenceType.CODE_DERIVED) # SQL LEFT JOIN line 462
|
||
fm_28d_change_metadata.set_evidence("semantic_contract", EvidenceType.DRAFT_DERIVED)
|
||
fm_28d_change_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED)
|
||
fm_28d_change_metadata.set_evidence("unit", EvidenceType.CODE_DERIVED)
|
||
fm_28d_change_metadata.set_evidence("time_window", EvidenceType.CODE_DERIVED) # days=28
|
||
fm_28d_change_metadata.set_evidence("output_type", EvidenceType.CODE_DERIVED)
|
||
fm_28d_change_metadata.set_evidence("placeholder_type", EvidenceType.MIXED)
|
||
fm_28d_change_metadata.set_evidence("format_hint", EvidenceType.CODE_DERIVED) # round(change, 2)
|
||
fm_28d_change_metadata.set_evidence("example_output", EvidenceType.DRAFT_DERIVED)
|
||
fm_28d_change_metadata.set_evidence("minimum_data_requirements", EvidenceType.CODE_DERIVED) # len(data) < 2
|
||
fm_28d_change_metadata.set_evidence("quality_filter_policy", EvidenceType.DRAFT_DERIVED)
|
||
fm_28d_change_metadata.set_evidence("confidence_logic", EvidenceType.DRAFT_DERIVED)
|
||
fm_28d_change_metadata.set_evidence("missing_value_policy", EvidenceType.CODE_DERIVED)
|
||
fm_28d_change_metadata.set_evidence("known_limitations", EvidenceType.CODE_DERIVED) # formula from body_metrics.py:453-501
|
||
fm_28d_change_metadata.set_evidence("layer_1_decision", EvidenceType.CODE_DERIVED)
|
||
fm_28d_change_metadata.set_evidence("layer_2a_decision", EvidenceType.CODE_DERIVED)
|
||
fm_28d_change_metadata.set_evidence("layer_2b_reuse_possible", EvidenceType.TO_VERIFY)
|
||
fm_28d_change_metadata.set_evidence("architecture_alignment", EvidenceType.CODE_DERIVED)
|
||
fm_28d_change_metadata.set_evidence("issue_53_alignment", EvidenceType.CODE_DERIVED)
|
||
|
||
register_placeholder(fm_28d_change_metadata)
|
||
|
||
# ── lbm_28d_change ───────────────────────────────────────────────────────
|
||
|
||
lbm_28d_change_metadata = PlaceholderMetadata(
|
||
key="lbm_28d_change",
|
||
category="Körper",
|
||
description="Magermasse Änderung 28d (kg)",
|
||
|
||
# Technical
|
||
resolver_module="backend/placeholder_resolver.py",
|
||
resolver_function="_safe_float('lbm_28d_change', decimals=2)",
|
||
data_layer_module="backend/data_layer/body_metrics.py",
|
||
data_layer_function="calculate_lbm_28d_change",
|
||
source_tables=["weight_log", "caliper_log"],
|
||
|
||
# Semantic
|
||
semantic_contract=(
|
||
"Liefert die Veränderung der Magermasse in Kilogramm über 28 Tage. "
|
||
"Positive Werte bedeuten Zunahme, negative Werte Verlust."
|
||
),
|
||
business_meaning="Kernindikator für Muskelerhalt / Muskelaufbau im Verlauf",
|
||
unit="kg",
|
||
time_window="28d",
|
||
output_type=OutputType.NUMERIC,
|
||
placeholder_type=PlaceholderType.INTERPRETED,
|
||
format_hint="Delta in kg (2 Dezimalstellen)",
|
||
example_output="+0.3",
|
||
|
||
# Quality
|
||
minimum_data_requirements="Analog zu fm_28d_change: Min 2 Einträge mit weight + body_fat_pct am selben Datum",
|
||
quality_filter_policy="Messfehler und methodische Inkonsistenzen berücksichtigen",
|
||
confidence_logic="Analog zu fm_28d_change",
|
||
missing_value_policy=MissingValuePolicy(
|
||
available=False,
|
||
value_raw=None,
|
||
missing_reason="insufficient_data",
|
||
legacy_display="nicht verfügbar"
|
||
),
|
||
known_limitations=(
|
||
"LBM-BERECHNUNG: LBM = weight_kg - FM = weight_kg - (weight_kg × body_fat_pct / 100). "
|
||
"DELTA: recent_lbm - oldest_lbm. "
|
||
"SAME-DATE REQUIREMENT: Erfordert weight + caliper am SELBEN Tag. "
|
||
"INDIREKT ABGELEITET: LBM ist meist indirekt (weight - FM), nicht direkt gemessen. "
|
||
"MESSMETHODEN-SENSITIVITÄT: Änderungen in BF-Messmethode beeinflussen LBM. "
|
||
"ÜBERSCHÄTZUNGSRISIKO: Ohne gute Messbasis leicht überschätzt. "
|
||
"ROUNDING: 2 Dezimalstellen. "
|
||
"Rechnerische Artefakte möglich wenn BF-/Gewichtskombination unstabil."
|
||
),
|
||
|
||
# Architecture
|
||
layer_1_decision="Data Layer (body_metrics.calculate_lbm_28d_change → _calculate_body_composition_change)",
|
||
layer_2a_decision="Placeholder Resolver (_safe_float für formatting)",
|
||
layer_2b_reuse_possible="Ja - LBM Delta direkt nutzbar",
|
||
architecture_alignment="Phase 0c conform",
|
||
issue_53_alignment="Layer-Trennung vollständig"
|
||
)
|
||
|
||
# Evidence tagging
|
||
lbm_28d_change_metadata.set_evidence("category", EvidenceType.CODE_DERIVED)
|
||
lbm_28d_change_metadata.set_evidence("resolver_module", EvidenceType.CODE_DERIVED)
|
||
lbm_28d_change_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED) # line 490
|
||
lbm_28d_change_metadata.set_evidence("data_layer_module", EvidenceType.CODE_DERIVED)
|
||
lbm_28d_change_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED) # body_metrics.py:448
|
||
lbm_28d_change_metadata.set_evidence("source_tables", EvidenceType.CODE_DERIVED)
|
||
lbm_28d_change_metadata.set_evidence("semantic_contract", EvidenceType.DRAFT_DERIVED)
|
||
lbm_28d_change_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED)
|
||
lbm_28d_change_metadata.set_evidence("unit", EvidenceType.CODE_DERIVED)
|
||
lbm_28d_change_metadata.set_evidence("time_window", EvidenceType.CODE_DERIVED)
|
||
lbm_28d_change_metadata.set_evidence("output_type", EvidenceType.CODE_DERIVED)
|
||
lbm_28d_change_metadata.set_evidence("placeholder_type", EvidenceType.MIXED)
|
||
lbm_28d_change_metadata.set_evidence("format_hint", EvidenceType.CODE_DERIVED)
|
||
lbm_28d_change_metadata.set_evidence("example_output", EvidenceType.DRAFT_DERIVED)
|
||
lbm_28d_change_metadata.set_evidence("minimum_data_requirements", EvidenceType.MIXED)
|
||
lbm_28d_change_metadata.set_evidence("quality_filter_policy", EvidenceType.DRAFT_DERIVED)
|
||
lbm_28d_change_metadata.set_evidence("confidence_logic", EvidenceType.MIXED)
|
||
lbm_28d_change_metadata.set_evidence("missing_value_policy", EvidenceType.CODE_DERIVED)
|
||
lbm_28d_change_metadata.set_evidence("known_limitations", EvidenceType.CODE_DERIVED)
|
||
lbm_28d_change_metadata.set_evidence("layer_1_decision", EvidenceType.CODE_DERIVED)
|
||
lbm_28d_change_metadata.set_evidence("layer_2a_decision", EvidenceType.CODE_DERIVED)
|
||
lbm_28d_change_metadata.set_evidence("layer_2b_reuse_possible", EvidenceType.TO_VERIFY)
|
||
lbm_28d_change_metadata.set_evidence("architecture_alignment", EvidenceType.CODE_DERIVED)
|
||
lbm_28d_change_metadata.set_evidence("issue_53_alignment", EvidenceType.CODE_DERIVED)
|
||
|
||
register_placeholder(lbm_28d_change_metadata)
|
||
|
||
|
||
# ── waist_hip_ratio ──────────────────────────────────────────────────────
|
||
|
||
waist_hip_ratio_metadata = PlaceholderMetadata(
|
||
key="waist_hip_ratio",
|
||
category="Körper",
|
||
description="Taille/Hüfte-Verhältnis",
|
||
|
||
# Technical
|
||
resolver_module="backend/placeholder_resolver.py",
|
||
resolver_function="_safe_float('waist_hip_ratio', decimals=3)",
|
||
data_layer_module="backend/data_layer/body_metrics.py",
|
||
data_layer_function="calculate_waist_hip_ratio",
|
||
source_tables=["circumference_log"],
|
||
|
||
# Semantic
|
||
semantic_contract=(
|
||
"Liefert das Verhältnis aus aktuellem Taillen- und Hüftumfang als dimensionslosen Quotienten."
|
||
),
|
||
business_meaning="Ergänzender Form- und Verteilungsindikator",
|
||
unit="ratio",
|
||
time_window="latest",
|
||
output_type=OutputType.NUMERIC,
|
||
placeholder_type=PlaceholderType.INTERPRETED,
|
||
format_hint="Quotient (3 Dezimalstellen), z.B. 0.91",
|
||
example_output="0.89",
|
||
|
||
# Quality
|
||
minimum_data_requirements="Je 1 valider aktueller Taillen- UND Hüftwert (auf selben Datum)",
|
||
quality_filter_policy="Null-/Fehlwerte und unplausible Umfänge ausschließen",
|
||
confidence_logic=(
|
||
"high: beide Maße aktuell und konsistent vorhanden, "
|
||
"insufficient: eines fehlt"
|
||
),
|
||
missing_value_policy=MissingValuePolicy(
|
||
available=False,
|
||
value_raw=None,
|
||
missing_reason="insufficient_data",
|
||
legacy_display="nicht verfügbar"
|
||
),
|
||
known_limitations=(
|
||
"BERECHNUNG: ratio = c_waist / c_hip. "
|
||
"SAME-DATE REQUIREMENT: Erfordert BEIDE Messungen auf SELBEN Datum "
|
||
"(latest entry WHERE c_waist IS NOT NULL AND c_hip IS NOT NULL). "
|
||
"ROUNDING: 3 Dezimalstellen (round(ratio, 3)). "
|
||
"ERGÄNZENDER INDIKATOR: Nicht zentraler Fortschrittsindikator. "
|
||
"Ohne konsistente Umfangsmessung wenig wertvoll. "
|
||
"Division durch 0 verhindert durch SQL WHERE c_hip IS NOT NULL."
|
||
),
|
||
|
||
# Architecture
|
||
layer_1_decision="Data Layer (body_metrics.calculate_waist_hip_ratio)",
|
||
layer_2a_decision="Placeholder Resolver (_safe_float für formatting)",
|
||
layer_2b_reuse_possible="Ja - Ratio direkt nutzbar",
|
||
architecture_alignment="Phase 0c conform",
|
||
issue_53_alignment="Layer-Trennung vollständig"
|
||
)
|
||
|
||
# Evidence tagging
|
||
waist_hip_ratio_metadata.set_evidence("category", EvidenceType.CODE_DERIVED)
|
||
waist_hip_ratio_metadata.set_evidence("resolver_module", EvidenceType.CODE_DERIVED)
|
||
waist_hip_ratio_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED) # line 496
|
||
waist_hip_ratio_metadata.set_evidence("data_layer_module", EvidenceType.CODE_DERIVED)
|
||
waist_hip_ratio_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED) # body_metrics.py:570
|
||
waist_hip_ratio_metadata.set_evidence("source_tables", EvidenceType.CODE_DERIVED) # SQL line 574
|
||
waist_hip_ratio_metadata.set_evidence("semantic_contract", EvidenceType.DRAFT_DERIVED)
|
||
waist_hip_ratio_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED)
|
||
waist_hip_ratio_metadata.set_evidence("unit", EvidenceType.MIXED) # ratio is dimensionless
|
||
waist_hip_ratio_metadata.set_evidence("time_window", EvidenceType.CODE_DERIVED) # ORDER BY date DESC LIMIT 1
|
||
waist_hip_ratio_metadata.set_evidence("output_type", EvidenceType.CODE_DERIVED)
|
||
waist_hip_ratio_metadata.set_evidence("placeholder_type", EvidenceType.MIXED)
|
||
waist_hip_ratio_metadata.set_evidence("format_hint", EvidenceType.CODE_DERIVED) # round(ratio, 3)
|
||
waist_hip_ratio_metadata.set_evidence("example_output", EvidenceType.DRAFT_DERIVED)
|
||
waist_hip_ratio_metadata.set_evidence("minimum_data_requirements", EvidenceType.CODE_DERIVED)
|
||
waist_hip_ratio_metadata.set_evidence("quality_filter_policy", EvidenceType.DRAFT_DERIVED)
|
||
waist_hip_ratio_metadata.set_evidence("confidence_logic", EvidenceType.MIXED)
|
||
waist_hip_ratio_metadata.set_evidence("missing_value_policy", EvidenceType.CODE_DERIVED)
|
||
waist_hip_ratio_metadata.set_evidence("known_limitations", EvidenceType.CODE_DERIVED)
|
||
waist_hip_ratio_metadata.set_evidence("layer_1_decision", EvidenceType.CODE_DERIVED)
|
||
waist_hip_ratio_metadata.set_evidence("layer_2a_decision", EvidenceType.CODE_DERIVED)
|
||
waist_hip_ratio_metadata.set_evidence("layer_2b_reuse_possible", EvidenceType.TO_VERIFY)
|
||
waist_hip_ratio_metadata.set_evidence("architecture_alignment", EvidenceType.CODE_DERIVED)
|
||
waist_hip_ratio_metadata.set_evidence("issue_53_alignment", EvidenceType.CODE_DERIVED)
|
||
|
||
register_placeholder(waist_hip_ratio_metadata)
|
||
|
||
# ── recomposition_quadrant ───────────────────────────────────────────────
|
||
|
||
recomposition_quadrant_metadata = PlaceholderMetadata(
|
||
key="recomposition_quadrant",
|
||
category="Körper",
|
||
description="Rekomposition-Status",
|
||
|
||
# Technical
|
||
resolver_module="backend/placeholder_resolver.py",
|
||
resolver_function="_safe_string('recomposition_quadrant')",
|
||
data_layer_module="backend/data_layer/body_metrics.py",
|
||
data_layer_function="calculate_recomposition_quadrant",
|
||
source_tables=["weight_log", "caliper_log"],
|
||
|
||
# Semantic
|
||
semantic_contract=(
|
||
"Klassifiziert die 28d-Körperentwicklung anhand von Fettmassen- und Magermassenveränderung "
|
||
"in einen fachlich definierten Quadranten bzw. Statusraum."
|
||
),
|
||
business_meaning="Hochwertiger Diagnose- und Syntheseindikator für Recomposition",
|
||
unit="category",
|
||
time_window="28d",
|
||
output_type=OutputType.STRING,
|
||
placeholder_type=PlaceholderType.INTERPRETED,
|
||
format_hint="String: optimal | cut_with_risk | bulk | unfavorable",
|
||
example_output="optimal",
|
||
|
||
# Quality
|
||
minimum_data_requirements="Belastbare Verfügbarkeit von fm_28d_change UND lbm_28d_change",
|
||
quality_filter_policy=(
|
||
"Wenn die zugrunde liegenden Deltas unzureichend oder methodisch unsicher sind, "
|
||
"darf keine harte Quadrantenklassifikation erfolgen"
|
||
),
|
||
confidence_logic=(
|
||
"Aus den Confidence-Werten von fm_28d_change und lbm_28d_change ableiten (Minimum-Prinzip)"
|
||
),
|
||
missing_value_policy=MissingValuePolicy(
|
||
available=False,
|
||
value_raw=None,
|
||
missing_reason="insufficient_data",
|
||
legacy_display="nicht verfügbar"
|
||
),
|
||
known_limitations=(
|
||
"QUADRANTEN-LOGIK (pure sign-based):\n"
|
||
" FM < 0, LBM > 0 → 'optimal' (Fett ab, Muskel auf - Recomposition)\n"
|
||
" FM < 0, LBM < 0 → 'cut_with_risk' (Fett ab, Muskel ab - aggressiver Cut)\n"
|
||
" FM > 0, LBM > 0 → 'bulk' (Fett auf, Muskel auf - Bulking-Phase)\n"
|
||
" FM > 0, LBM < 0 → 'unfavorable' (Fett auf, Muskel ab - worst case)\n\n"
|
||
"SIGN-BASED THRESHOLDS: Rein basierend auf Vorzeichen (< 0 vs. > 0). "
|
||
"KEINE TOLERANZ-ZONE: Kleine Änderungen nahe 0 werden als signifikant behandelt. "
|
||
"KEINE 'STABLE' KATEGORIE: Changes near 0 können Quadrant-Flips verursachen. "
|
||
"MESSMETHODEN-ABHÄNGIGKEIT: Stark abhängig von Qualität der Körperkompositionsdaten. "
|
||
"Methodenwechsel bei BF-Messung kann Quadranten verzerren."
|
||
),
|
||
|
||
# Architecture
|
||
layer_1_decision="Data Layer (body_metrics.calculate_recomposition_quadrant)",
|
||
layer_2a_decision="Placeholder Resolver (_safe_string für formatting)",
|
||
layer_2b_reuse_possible="Ja - Quadrant category direkt nutzbar",
|
||
architecture_alignment="Phase 0c conform",
|
||
issue_53_alignment="Layer-Trennung vollständig"
|
||
)
|
||
|
||
# Evidence tagging
|
||
recomposition_quadrant_metadata.set_evidence("category", EvidenceType.CODE_DERIVED)
|
||
recomposition_quadrant_metadata.set_evidence("resolver_module", EvidenceType.CODE_DERIVED)
|
||
recomposition_quadrant_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED) # _safe_string mapping
|
||
recomposition_quadrant_metadata.set_evidence("data_layer_module", EvidenceType.CODE_DERIVED)
|
||
recomposition_quadrant_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED) # body_metrics.py:594
|
||
recomposition_quadrant_metadata.set_evidence("source_tables", EvidenceType.CODE_DERIVED) # via fm/lbm functions
|
||
recomposition_quadrant_metadata.set_evidence("semantic_contract", EvidenceType.DRAFT_DERIVED)
|
||
recomposition_quadrant_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED)
|
||
recomposition_quadrant_metadata.set_evidence("unit", EvidenceType.MIXED)
|
||
recomposition_quadrant_metadata.set_evidence("time_window", EvidenceType.CODE_DERIVED) # 28d via fm/lbm
|
||
recomposition_quadrant_metadata.set_evidence("output_type", EvidenceType.CODE_DERIVED)
|
||
recomposition_quadrant_metadata.set_evidence("placeholder_type", EvidenceType.MIXED)
|
||
recomposition_quadrant_metadata.set_evidence("format_hint", EvidenceType.CODE_DERIVED) # return strings line 606-615
|
||
recomposition_quadrant_metadata.set_evidence("example_output", EvidenceType.CODE_DERIVED)
|
||
recomposition_quadrant_metadata.set_evidence("minimum_data_requirements", EvidenceType.MIXED)
|
||
recomposition_quadrant_metadata.set_evidence("quality_filter_policy", EvidenceType.DRAFT_DERIVED)
|
||
recomposition_quadrant_metadata.set_evidence("confidence_logic", EvidenceType.DRAFT_DERIVED)
|
||
recomposition_quadrant_metadata.set_evidence("missing_value_policy", EvidenceType.CODE_DERIVED)
|
||
recomposition_quadrant_metadata.set_evidence("known_limitations", EvidenceType.CODE_DERIVED) # logic from body_metrics.py:594-616
|
||
recomposition_quadrant_metadata.set_evidence("layer_1_decision", EvidenceType.CODE_DERIVED)
|
||
recomposition_quadrant_metadata.set_evidence("layer_2a_decision", EvidenceType.CODE_DERIVED)
|
||
recomposition_quadrant_metadata.set_evidence("layer_2b_reuse_possible", EvidenceType.TO_VERIFY)
|
||
recomposition_quadrant_metadata.set_evidence("architecture_alignment", EvidenceType.CODE_DERIVED)
|
||
recomposition_quadrant_metadata.set_evidence("issue_53_alignment", EvidenceType.CODE_DERIVED)
|
||
|
||
register_placeholder(recomposition_quadrant_metadata)
|
||
|
||
|
||
# ═════════════════════════════════════════════════════════════════════════
|
||
# CIRCUMFERENCE DELTAS (5 Placeholders)
|
||
# ═════════════════════════════════════════════════════════════════════════
|
||
|
||
# Common metadata for all 5 circumference deltas
|
||
circumference_delta_common = {
|
||
"category": "Körper",
|
||
"resolver_module": "backend/placeholder_resolver.py",
|
||
"data_layer_module": "backend/data_layer/body_metrics.py",
|
||
"source_tables": ["circumference_log"],
|
||
"time_window": "28d",
|
||
"output_type": OutputType.NUMERIC,
|
||
"placeholder_type": PlaceholderType.INTERPRETED,
|
||
"format_hint": "Delta in cm (1 Dezimalstelle)",
|
||
"minimum_data_requirements": (
|
||
"Mindestens 2 valide Messungen: "
|
||
"1 im 28d-Fenster (most recent), 1 VOR dem Fenster (oldest before window)"
|
||
),
|
||
"quality_filter_policy": "Messfehler, Ausreißer und inkonsistente Einträge berücksichtigen",
|
||
"confidence_logic": (
|
||
"high: mehrere konsistente Messpunkte im 28d-Fenster, "
|
||
"medium: Mindestanforderung knapp erfüllt, "
|
||
"low: Daten formal reichen aber dünn, "
|
||
"insufficient: < 2 valide Messpunkte"
|
||
),
|
||
"missing_value_policy": MissingValuePolicy(
|
||
available=False,
|
||
value_raw=None,
|
||
missing_reason="insufficient_data",
|
||
legacy_display="nicht verfügbar"
|
||
),
|
||
"known_limitations": (
|
||
"REFERENZLOGIK (KRITISCH): Vergleicht aktuellsten Wert IM 28d-Fenster "
|
||
"mit ältestem Wert VOR dem 28d-Fenster. "
|
||
"NICHT: Delta zwischen Start und Ende des Fensters. "
|
||
"SQL: recent = latest WHERE date >= CURRENT_DATE - 28d, "
|
||
"oldest = latest WHERE date < CURRENT_DATE - 28d. "
|
||
"SPAN kann > 28d sein bei lückenhaften Messungen. "
|
||
"Beispiel: Messung heute + Messung vor 40 Tagen → Delta span = 40d. "
|
||
"ROUNDING: 1 Dezimalstelle (round(change, 1)). "
|
||
"Bei sehr unregelmäßigen Messungen kann dies zu irreführenden Deltas führen."
|
||
),
|
||
"layer_1_decision": "Data Layer (body_metrics._calculate_circumference_delta)",
|
||
"layer_2a_decision": "Placeholder Resolver (_safe_float für formatting)",
|
||
"layer_2b_reuse_possible": "Ja - Delta-Wert direkt nutzbar",
|
||
"architecture_alignment": "Phase 0c conform",
|
||
"issue_53_alignment": "Layer-Trennung vollständig"
|
||
}
|
||
|
||
# Common evidence for circumference deltas
|
||
circ_delta_evidence = {
|
||
"category": EvidenceType.CODE_DERIVED,
|
||
"resolver_module": EvidenceType.CODE_DERIVED,
|
||
"data_layer_module": EvidenceType.CODE_DERIVED,
|
||
"source_tables": EvidenceType.CODE_DERIVED,
|
||
"time_window": EvidenceType.CODE_DERIVED,
|
||
"output_type": EvidenceType.CODE_DERIVED,
|
||
"placeholder_type": EvidenceType.MIXED,
|
||
"format_hint": EvidenceType.CODE_DERIVED,
|
||
"minimum_data_requirements": EvidenceType.CODE_DERIVED, # SQL shows 2-query pattern
|
||
"quality_filter_policy": EvidenceType.DRAFT_DERIVED,
|
||
"confidence_logic": EvidenceType.DRAFT_DERIVED,
|
||
"missing_value_policy": EvidenceType.CODE_DERIVED,
|
||
"known_limitations": EvidenceType.CODE_DERIVED, # from _calculate_circumference_delta body_metrics.py:534
|
||
"layer_1_decision": EvidenceType.CODE_DERIVED,
|
||
"layer_2a_decision": EvidenceType.CODE_DERIVED,
|
||
"layer_2b_reuse_possible": EvidenceType.TO_VERIFY,
|
||
"architecture_alignment": EvidenceType.CODE_DERIVED,
|
||
"issue_53_alignment": EvidenceType.CODE_DERIVED,
|
||
}
|
||
|
||
# ── waist_28d_delta ──────────────────────────────────────────────────────
|
||
|
||
waist_28d_delta_metadata = PlaceholderMetadata(
|
||
key="waist_28d_delta",
|
||
description="Taillenumfang Änderung 28d (cm)",
|
||
resolver_function="_safe_float('waist_28d_delta', decimals=1)",
|
||
data_layer_function="calculate_waist_28d_delta",
|
||
semantic_contract=(
|
||
"Liefert die Veränderung des Taillenumfangs in Zentimetern über 28 Tage. "
|
||
"Negative Werte bedeuten Reduktion, positive Werte Zunahme."
|
||
),
|
||
business_meaning="Zentraler Delta-Indikator für Körperfett-/Körperformveränderung",
|
||
unit="cm",
|
||
example_output="-2.1",
|
||
**circumference_delta_common
|
||
)
|
||
waist_28d_delta_metadata.evidence.update(circ_delta_evidence)
|
||
waist_28d_delta_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED) # line 491
|
||
waist_28d_delta_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED) # body_metrics.py:506
|
||
waist_28d_delta_metadata.set_evidence("semantic_contract", EvidenceType.DRAFT_DERIVED)
|
||
waist_28d_delta_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED)
|
||
waist_28d_delta_metadata.set_evidence("unit", EvidenceType.CODE_DERIVED)
|
||
waist_28d_delta_metadata.set_evidence("example_output", EvidenceType.DRAFT_DERIVED)
|
||
register_placeholder(waist_28d_delta_metadata)
|
||
|
||
# ── arm_28d_delta ────────────────────────────────────────────────────────
|
||
|
||
arm_28d_delta_metadata = PlaceholderMetadata(
|
||
key="arm_28d_delta",
|
||
description="Armumfang Änderung 28d (cm)",
|
||
resolver_function="_safe_float('arm_28d_delta', decimals=1)",
|
||
data_layer_function="calculate_arm_28d_delta",
|
||
semantic_contract=(
|
||
"Liefert die Veränderung des Armumfangs in Zentimetern über 28 Tage. "
|
||
"Positive Werte bedeuten Zunahme, negative Werte Reduktion."
|
||
),
|
||
business_meaning="Ergänzender Umfangsindikator für detaillierte Körperentwicklungsanalysen",
|
||
unit="cm",
|
||
example_output="+0.6",
|
||
**circumference_delta_common
|
||
)
|
||
arm_28d_delta_metadata.evidence.update(circ_delta_evidence)
|
||
arm_28d_delta_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED) # line 494
|
||
arm_28d_delta_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED) # body_metrics.py:521
|
||
arm_28d_delta_metadata.set_evidence("semantic_contract", EvidenceType.DRAFT_DERIVED)
|
||
arm_28d_delta_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED)
|
||
arm_28d_delta_metadata.set_evidence("unit", EvidenceType.CODE_DERIVED)
|
||
arm_28d_delta_metadata.set_evidence("example_output", EvidenceType.DRAFT_DERIVED)
|
||
register_placeholder(arm_28d_delta_metadata)
|
||
|
||
# ── chest_28d_delta ──────────────────────────────────────────────────────
|
||
|
||
chest_28d_delta_metadata = PlaceholderMetadata(
|
||
key="chest_28d_delta",
|
||
description="Brustumfang Änderung 28d (cm)",
|
||
resolver_function="_safe_float('chest_28d_delta', decimals=1)",
|
||
data_layer_function="calculate_chest_28d_delta",
|
||
semantic_contract=(
|
||
"Liefert die Veränderung des Brustumfangs in Zentimetern über 28 Tage. "
|
||
"Positive Werte bedeuten Zunahme, negative Werte Reduktion."
|
||
),
|
||
business_meaning="Ergänzender Umfangsindikator für Oberkörper-Entwicklungsanalysen",
|
||
unit="cm",
|
||
example_output="+0.4",
|
||
**circumference_delta_common
|
||
)
|
||
chest_28d_delta_metadata.evidence.update(circ_delta_evidence)
|
||
chest_28d_delta_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED) # line 493
|
||
chest_28d_delta_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED) # body_metrics.py:516
|
||
chest_28d_delta_metadata.set_evidence("semantic_contract", EvidenceType.DRAFT_DERIVED)
|
||
chest_28d_delta_metadata.set_evidence("business_meaning", EvidenceType.MIXED)
|
||
chest_28d_delta_metadata.set_evidence("unit", EvidenceType.CODE_DERIVED)
|
||
chest_28d_delta_metadata.set_evidence("example_output", EvidenceType.MIXED)
|
||
register_placeholder(chest_28d_delta_metadata)
|
||
|
||
# ── hip_28d_delta ────────────────────────────────────────────────────────
|
||
|
||
hip_28d_delta_metadata = PlaceholderMetadata(
|
||
key="hip_28d_delta",
|
||
description="Hüftumfang Änderung 28d (cm)",
|
||
resolver_function="_safe_float('hip_28d_delta', decimals=1)",
|
||
data_layer_function="calculate_hip_28d_delta",
|
||
semantic_contract=(
|
||
"Liefert die Veränderung des Hüftumfangs in Zentimetern über 28 Tage. "
|
||
"Positive Werte bedeuten Zunahme, negative Werte Reduktion."
|
||
),
|
||
business_meaning="Ergänzender Umfangsindikator für Unterkörper-Entwicklungsanalysen",
|
||
unit="cm",
|
||
example_output="-0.8",
|
||
**circumference_delta_common
|
||
)
|
||
hip_28d_delta_metadata.evidence.update(circ_delta_evidence)
|
||
hip_28d_delta_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED) # line 492
|
||
hip_28d_delta_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED) # body_metrics.py:511
|
||
hip_28d_delta_metadata.set_evidence("semantic_contract", EvidenceType.DRAFT_DERIVED)
|
||
hip_28d_delta_metadata.set_evidence("business_meaning", EvidenceType.MIXED)
|
||
hip_28d_delta_metadata.set_evidence("unit", EvidenceType.CODE_DERIVED)
|
||
hip_28d_delta_metadata.set_evidence("example_output", EvidenceType.DRAFT_DERIVED)
|
||
register_placeholder(hip_28d_delta_metadata)
|
||
|
||
# ── thigh_28d_delta ──────────────────────────────────────────────────────
|
||
|
||
thigh_28d_delta_metadata = PlaceholderMetadata(
|
||
key="thigh_28d_delta",
|
||
description="Oberschenkelumfang Änderung 28d (cm)",
|
||
resolver_function="_safe_float('thigh_28d_delta', decimals=1)",
|
||
data_layer_function="calculate_thigh_28d_delta",
|
||
semantic_contract=(
|
||
"Liefert die Veränderung des Oberschenkelumfangs in Zentimetern über 28 Tage. "
|
||
"Positive Werte bedeuten Zunahme, negative Werte Reduktion."
|
||
),
|
||
business_meaning="Ergänzender Umfangsindikator für Beinmuskulatur-Entwicklung",
|
||
unit="cm",
|
||
example_output="+0.3",
|
||
**circumference_delta_common
|
||
)
|
||
thigh_28d_delta_metadata.evidence.update(circ_delta_evidence)
|
||
thigh_28d_delta_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED) # line 495
|
||
thigh_28d_delta_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED) # body_metrics.py:526
|
||
thigh_28d_delta_metadata.set_evidence("semantic_contract", EvidenceType.DRAFT_DERIVED)
|
||
thigh_28d_delta_metadata.set_evidence("business_meaning", EvidenceType.MIXED)
|
||
thigh_28d_delta_metadata.set_evidence("unit", EvidenceType.CODE_DERIVED)
|
||
thigh_28d_delta_metadata.set_evidence("example_output", EvidenceType.MIXED)
|
||
register_placeholder(thigh_28d_delta_metadata)
|
||
|
||
# ═════════════════════════════════════════════════════════════════════════
|
||
# SUMMARIES (2 Placeholders)
|
||
# ═════════════════════════════════════════════════════════════════════════
|
||
|
||
# ── caliper_summary ──────────────────────────────────────────────────────
|
||
|
||
caliper_summary_metadata = PlaceholderMetadata(
|
||
key="caliper_summary",
|
||
category="Körper",
|
||
description="Caliper/Körperfett-Zusammenfassung",
|
||
|
||
# Technical
|
||
resolver_module="backend/placeholder_resolver.py",
|
||
resolver_function="get_caliper_summary",
|
||
data_layer_module="backend/data_layer/body_metrics.py",
|
||
data_layer_function="get_body_composition_data",
|
||
source_tables=["caliper_log"],
|
||
|
||
# Semantic
|
||
semantic_contract=(
|
||
"Liefert eine strukturierte textuelle Zusammenfassung der verfügbaren "
|
||
"Caliper-bezogenen Körperfettinformationen. Format: 'KF% (Methode am Datum)'."
|
||
),
|
||
business_meaning="Bequemer High-Level-Einstieg für narrative Körperreports",
|
||
unit="text",
|
||
time_window="latest",
|
||
output_type=OutputType.TEXT_SUMMARY,
|
||
placeholder_type=PlaceholderType.INTERPRETED,
|
||
format_hint="String: '18.7% (jackson_pollock am 2026-03-30)'",
|
||
example_output="18.7% (jackson_pollock am 2026-03-30)",
|
||
|
||
# Quality
|
||
minimum_data_requirements="Mindestens 1 valider Caliper-/BF-Bezug für Snapshot-Teil",
|
||
quality_filter_policy="Unsichere Bestandteile müssen kenntlich gemacht oder ausgelassen werden",
|
||
confidence_logic="Abgeleitet aus get_body_composition_data (high wenn vorhanden)",
|
||
missing_value_policy=MissingValuePolicy(
|
||
available=False,
|
||
value_raw=None,
|
||
missing_reason="no_data",
|
||
legacy_display="nicht verfügbar"
|
||
),
|
||
known_limitations=(
|
||
"TEXT-FORMAT: Aktuell Freitext-String, nicht strukturiertes JSON. "
|
||
"Canonical empfiehlt: Strukturierte JSON-Summary statt Freitext. "
|
||
"NO-CHANGE REGEL: Format bleibt als Text-String erhalten (wie implementiert). "
|
||
"LOOKBACK: Nutzt get_body_composition_data mit 90d lookback. "
|
||
"FORMAT: f'{body_fat_pct:.1f}% ({method} am {date})'. "
|
||
"Als Freitext weniger stabil für Multi-Stage-Prompts. "
|
||
"Gefahr von semantischer Unschärfe bei komplexeren Auswertungen."
|
||
),
|
||
|
||
# Architecture
|
||
layer_1_decision="Data Layer (body_metrics.get_body_composition_data)",
|
||
layer_2a_decision="Placeholder Resolver (formatting zu Text-String)",
|
||
layer_2b_reuse_possible="Teilweise - besser strukturiert für Charts",
|
||
architecture_alignment="Phase 0c conform (nutzt Data Layer)",
|
||
issue_53_alignment="Layer-Trennung etabliert, aber Text-Summary suboptimal"
|
||
)
|
||
|
||
# Evidence tagging
|
||
caliper_summary_metadata.set_evidence("category", EvidenceType.CODE_DERIVED)
|
||
caliper_summary_metadata.set_evidence("resolver_module", EvidenceType.CODE_DERIVED)
|
||
caliper_summary_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED) # line 1087
|
||
caliper_summary_metadata.set_evidence("data_layer_module", EvidenceType.CODE_DERIVED)
|
||
caliper_summary_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED)
|
||
caliper_summary_metadata.set_evidence("source_tables", EvidenceType.CODE_DERIVED)
|
||
caliper_summary_metadata.set_evidence("semantic_contract", EvidenceType.MIXED)
|
||
caliper_summary_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED)
|
||
caliper_summary_metadata.set_evidence("unit", EvidenceType.MIXED)
|
||
caliper_summary_metadata.set_evidence("time_window", EvidenceType.CODE_DERIVED)
|
||
caliper_summary_metadata.set_evidence("output_type", EvidenceType.CODE_DERIVED)
|
||
caliper_summary_metadata.set_evidence("placeholder_type", EvidenceType.MIXED)
|
||
caliper_summary_metadata.set_evidence("format_hint", EvidenceType.CODE_DERIVED) # resolver line 148
|
||
caliper_summary_metadata.set_evidence("example_output", EvidenceType.CODE_DERIVED)
|
||
caliper_summary_metadata.set_evidence("minimum_data_requirements", EvidenceType.MIXED)
|
||
caliper_summary_metadata.set_evidence("quality_filter_policy", EvidenceType.DRAFT_DERIVED)
|
||
caliper_summary_metadata.set_evidence("confidence_logic", EvidenceType.MIXED)
|
||
caliper_summary_metadata.set_evidence("missing_value_policy", EvidenceType.CODE_DERIVED)
|
||
caliper_summary_metadata.set_evidence("known_limitations", EvidenceType.MIXED)
|
||
caliper_summary_metadata.set_evidence("layer_1_decision", EvidenceType.CODE_DERIVED)
|
||
caliper_summary_metadata.set_evidence("layer_2a_decision", EvidenceType.CODE_DERIVED)
|
||
caliper_summary_metadata.set_evidence("layer_2b_reuse_possible", EvidenceType.TO_VERIFY)
|
||
caliper_summary_metadata.set_evidence("architecture_alignment", EvidenceType.CODE_DERIVED)
|
||
caliper_summary_metadata.set_evidence("issue_53_alignment", EvidenceType.MIXED)
|
||
|
||
register_placeholder(caliper_summary_metadata)
|
||
|
||
# ── circ_summary ─────────────────────────────────────────────────────────
|
||
|
||
circ_summary_metadata = PlaceholderMetadata(
|
||
key="circ_summary",
|
||
category="Körper",
|
||
description="Umfangs-Zusammenfassung",
|
||
|
||
# Technical
|
||
resolver_module="backend/placeholder_resolver.py",
|
||
resolver_function="get_circ_summary",
|
||
data_layer_module="backend/data_layer/body_metrics.py",
|
||
data_layer_function="get_circumference_summary_data",
|
||
source_tables=["circumference_log"],
|
||
|
||
# Semantic
|
||
semantic_contract=(
|
||
"Liefert eine standardisierte Zusammenfassung der relevanten Umfangsdaten. "
|
||
"Verwendet BEST-OF-EACH Strategie: Jeder Messpunkt zeigt seinen individuell "
|
||
"aktuellsten Wert (max 90d alt). Format: 'Punkt Wert cm (vor X Tagen), ...'"
|
||
),
|
||
business_meaning="High-Level-Kontext für Körperform- und Umfangsverlauf",
|
||
unit="text",
|
||
time_window="mixed",
|
||
output_type=OutputType.TEXT_SUMMARY,
|
||
placeholder_type=PlaceholderType.INTERPRETED,
|
||
format_hint="String mit allen verfügbaren Messpunkten und Alter",
|
||
example_output="Nacken 38.0cm (vor 2 Tagen), Taille 85.2cm (vor 5 Tagen)",
|
||
|
||
# Quality
|
||
minimum_data_requirements="Je nach enthaltenen Teilwerten (mindestens 1 Messpunkt)",
|
||
quality_filter_policy="Ausreißer und inkonsistente Messserien berücksichtigen",
|
||
confidence_logic="Aus calculate_confidence(len(measurements), 8, 'general') - je mehr Punkte desto höher",
|
||
missing_value_policy=MissingValuePolicy(
|
||
available=False,
|
||
value_raw=None,
|
||
missing_reason="no_data",
|
||
legacy_display="nicht verfügbar"
|
||
),
|
||
known_limitations=(
|
||
"BEST-OF-EACH STRATEGIE (KRITISCH): Jeder Umfangspunkt zeigt seinen individuell "
|
||
"aktuellsten Wert innerhalb 90d. "
|
||
"ERGEBNIS: Messungen sind NICHT notwendigerweise vom selben Datum. "
|
||
"Beispiel: Nacken vor 2 Tagen, Taille vor 5 Tagen, Hüfte vor 10 Tagen. "
|
||
"NICHT: Konsistenter Snapshot. "
|
||
"FÜR KONSISTENTE VERGLEICHE: Einzel-Deltas (waist_28d_delta etc.) nutzen. "
|
||
"TEXT-FORMAT: Freitext-String statt strukturiertes JSON. "
|
||
"Canonical empfiehlt: Strukturierte Summary. "
|
||
"NO-CHANGE REGEL: Format bleibt Text-String (wie implementiert). "
|
||
"Als Summary weniger präzise als einzelne Delta-Placeholder."
|
||
),
|
||
|
||
# Architecture
|
||
layer_1_decision="Data Layer (body_metrics.get_circumference_summary_data) - Best-of-Each Logic",
|
||
layer_2a_decision="Placeholder Resolver (formatting zu Text-String mit Altersangaben)",
|
||
layer_2b_reuse_possible="Teilweise - strukturierte Daten besser für Charts",
|
||
architecture_alignment="Phase 0c conform",
|
||
issue_53_alignment="Layer-Trennung etabliert, aber Text-Summary suboptimal"
|
||
)
|
||
|
||
# Evidence tagging
|
||
circ_summary_metadata.set_evidence("category", EvidenceType.CODE_DERIVED)
|
||
circ_summary_metadata.set_evidence("resolver_module", EvidenceType.CODE_DERIVED)
|
||
circ_summary_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED) # line 1088
|
||
circ_summary_metadata.set_evidence("data_layer_module", EvidenceType.CODE_DERIVED)
|
||
circ_summary_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED) # body_metrics.py:217
|
||
circ_summary_metadata.set_evidence("source_tables", EvidenceType.CODE_DERIVED)
|
||
circ_summary_metadata.set_evidence("semantic_contract", EvidenceType.MIXED) # Best-of-Each from code
|
||
circ_summary_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED)
|
||
circ_summary_metadata.set_evidence("unit", EvidenceType.MIXED)
|
||
circ_summary_metadata.set_evidence("time_window", EvidenceType.CODE_DERIVED) # Best-of-Each = mixed
|
||
circ_summary_metadata.set_evidence("output_type", EvidenceType.CODE_DERIVED)
|
||
circ_summary_metadata.set_evidence("placeholder_type", EvidenceType.MIXED)
|
||
circ_summary_metadata.set_evidence("format_hint", EvidenceType.CODE_DERIVED) # resolver formatting
|
||
circ_summary_metadata.set_evidence("example_output", EvidenceType.MIXED)
|
||
circ_summary_metadata.set_evidence("minimum_data_requirements", EvidenceType.MIXED)
|
||
circ_summary_metadata.set_evidence("quality_filter_policy", EvidenceType.DRAFT_DERIVED)
|
||
circ_summary_metadata.set_evidence("confidence_logic", EvidenceType.CODE_DERIVED) # body_metrics.py:296
|
||
circ_summary_metadata.set_evidence("missing_value_policy", EvidenceType.CODE_DERIVED)
|
||
circ_summary_metadata.set_evidence("known_limitations", EvidenceType.CODE_DERIVED) # Best-of-Each logic critical
|
||
circ_summary_metadata.set_evidence("layer_1_decision", EvidenceType.CODE_DERIVED)
|
||
circ_summary_metadata.set_evidence("layer_2a_decision", EvidenceType.CODE_DERIVED)
|
||
circ_summary_metadata.set_evidence("layer_2b_reuse_possible", EvidenceType.TO_VERIFY)
|
||
circ_summary_metadata.set_evidence("architecture_alignment", EvidenceType.CODE_DERIVED)
|
||
circ_summary_metadata.set_evidence("issue_53_alignment", EvidenceType.MIXED)
|
||
|
||
register_placeholder(circ_summary_metadata)
|
||
|
||
|
||
# Auto-register when module is imported
|
||
register_body_metrics()
|