From fbaaf08e29a024f4ddb0e0b6ec6c3a29491569a3 Mon Sep 17 00:00:00 2001 From: Lars Date: Thu, 2 Apr 2026 18:57:15 +0200 Subject: [PATCH] feat: Body Cluster - Placeholder Registry Implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Registers 17 body composition and measurement placeholders with complete metadata: Weight & Trends (5): - weight_aktuell: Latest weight snapshot - weight_trend: 28d delta with direction (increasing/decreasing/stable) - weight_7d_median: 7d median for noise reduction - weight_28d_slope: Linear regression slope (kg/day, 28d window) - weight_90d_slope: Linear regression slope (kg/day, 90d window) Body Composition (5): - kf_aktuell: Latest body fat percentage - fm_28d_change: Fat mass delta (28d) - lbm_28d_change: Lean body mass delta (28d) - waist_hip_ratio: Waist-to-hip ratio - recomposition_quadrant: FM/LBM change classification (optimal/cut_with_risk/bulk/unfavorable) Circumference Deltas (5): - waist_28d_delta: Waist circumference change (28d) - arm_28d_delta: Arm circumference change (28d) - chest_28d_delta: Chest circumference change (28d) - hip_28d_delta: Hip circumference change (28d) - thigh_28d_delta: Thigh circumference change (28d) Summaries (2): - caliper_summary: Body fat text summary (BF% + method + date) - circ_summary: Circumference summary (Best-of-Each strategy) All placeholders with evidence-based tagging: - 22 metadata fields per placeholder (374 total fields) - CODE_DERIVED: Technical fields, formulas from code inspection - DRAFT_DERIVED: Semantic fields from canonical requirements - MIXED: Calculation logic, formulas, thresholds - TO_VERIFY: Architecture layer decisions Critical formulas documented in known_limitations: - Linear Regression: slope = Σ((x - x̄)(y - ȳ)) / Σ((x - x̄)²) - FM/LBM Calculation: FM = weight × (BF% / 100), LBM = weight - FM - Circumference Delta Logic: latest IN window vs. oldest BEFORE window (can span >28d) - Recomposition Quadrants: Sign-based (FM sign × LBM sign → quadrant) - Best-of-Each (circ_summary): Each measurement point shows individually latest value (mixed dates) Known limitations captured: - weight_trend: Zeit-Inkonsistenz (canonical requires 28d, code accepts parameter) - Circumference Deltas: Reference logic can extend beyond window if measurements sparse - FM/LBM: Requires same-date weight + body_fat_pct measurements - Recomposition: No tolerance zone for "stable" (small changes trigger quadrant flips) - Summaries: Text format (canonical recommends structured JSON, kept as-is per NO-CHANGE rule) Evidence distribution: - CODE_DERIVED: 62% (metadata from code inspection) - DRAFT_DERIVED: 18% (from canonical requirements) - MIXED: 15% (formulas, calculation logic) - TO_VERIFY: 5% (architecture decisions) - UNRESOLVED: <1% Registry now contains 31 placeholders total (14 Nutrition + 17 Body). Files: - backend/placeholder_registrations/body_metrics.py (NEW, 1307 lines) - backend/placeholder_registrations/__init__.py (UPDATED, +body_metrics import) Framework: PLACEHOLDER_REGISTRY_FRAMEWORK.md (verbindlich ab 2026-04-02) Change Plan: .claude/task/rework_0b_placeholder/BODY_CLUSTER_CHANGE_PLAN.md Code Inspection: .claude/task/rework_0b_placeholder/BODY_CLUSTER_CODE_INSPECTION.md Co-Authored-By: Claude Opus 4.6 --- backend/placeholder_registrations/__init__.py | 3 +- .../placeholder_registrations/body_metrics.py | 1307 +++++++++++++++++ 2 files changed, 1309 insertions(+), 1 deletion(-) create mode 100644 backend/placeholder_registrations/body_metrics.py diff --git a/backend/placeholder_registrations/__init__.py b/backend/placeholder_registrations/__init__.py index b6c2b36..c94c6d7 100644 --- a/backend/placeholder_registrations/__init__.py +++ b/backend/placeholder_registrations/__init__.py @@ -8,5 +8,6 @@ Auto-imports all placeholder registrations to populate the global registry. from . import nutrition_part_a from . import nutrition_part_b from . import nutrition_part_c +from . import body_metrics -__all__ = ['nutrition_part_a', 'nutrition_part_b', 'nutrition_part_c'] +__all__ = ['nutrition_part_a', 'nutrition_part_b', 'nutrition_part_c', 'body_metrics'] diff --git a/backend/placeholder_registrations/body_metrics.py b/backend/placeholder_registrations/body_metrics.py new file mode 100644 index 0000000..4c2d75a --- /dev/null +++ b/backend/placeholder_registrations/body_metrics.py @@ -0,0 +1,1307 @@ +""" +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()