- caliper_summary + circ_summary used invalid PlaceholderType.TEXT_SUMMARY - TEXT_SUMMARY is OutputType, not PlaceholderType - Changed to PlaceholderType.INTERPRETED (summaries interpret raw data) Valid PlaceholderType values: ATOMIC, RAW_DATA, INTERPRETED, SCORE, META Valid OutputType values: NUMERIC, STRING, BOOLEAN, JSON, LIST, TEXT_SUMMARY
1308 lines
75 KiB
Python
1308 lines
75 KiB
Python
"""
|
||
Body Metrics Placeholder Registrations
|
||
|
||
Registers 17 body composition and measurement placeholders:
|
||
|
||
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.
|
||
Code inspection: backend/data_layer/body_metrics.py (830 lines)
|
||
"""
|
||
|
||
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()
|