mitai-jinkendo/backend/placeholder_registrations/body_metrics.py

1309 lines
75 KiB
Python
Raw Normal View History

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