""" 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.TEXT_SUMMARY, 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.TEXT_SUMMARY, 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()