238 lines
11 KiB
Python
238 lines
11 KiB
Python
|
|
"""
|
|||
|
|
Registry: BMI, Profil-Ziele (goal_weight, goal_bf_pct), body_progress_score.
|
|||
|
|
|
|||
|
|
Profilfelder sind unabhängig von der goals-Tabelle; operative Ziele über andere Keys.
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from placeholder_registry import (
|
|||
|
|
PlaceholderMetadata,
|
|||
|
|
MissingValuePolicy,
|
|||
|
|
EvidenceType,
|
|||
|
|
OutputType,
|
|||
|
|
PlaceholderType,
|
|||
|
|
register_placeholder,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def register_body_extras():
|
|||
|
|
bmi = PlaceholderMetadata(
|
|||
|
|
key="bmi",
|
|||
|
|
category="Körper",
|
|||
|
|
description="Body-Mass-Index aus letztem Gewicht und Profilgröße (cm)",
|
|||
|
|
resolver_module="backend/placeholder_resolver.py",
|
|||
|
|
resolver_function="calculate_bmi",
|
|||
|
|
data_layer_module="backend/data_layer/body_metrics.py",
|
|||
|
|
data_layer_function="get_bmi_data",
|
|||
|
|
source_tables=["profiles", "weight_log"],
|
|||
|
|
semantic_contract=(
|
|||
|
|
"BMI = Gewicht_kg / (Größe_m)² mit Größe_m = profiles.height / 100 "
|
|||
|
|
"und Gewicht = jüngster Eintrag in weight_log."
|
|||
|
|
),
|
|||
|
|
business_meaning="Standard-Körpermaß für Coaching und Risiko-Kontext",
|
|||
|
|
unit="kg/m²",
|
|||
|
|
time_window="latest weight + aktuelle Profilgröße",
|
|||
|
|
output_type=OutputType.NUMERIC,
|
|||
|
|
placeholder_type=PlaceholderType.RAW_DATA,
|
|||
|
|
format_hint="Eine Dezimalstelle, ohne Einheit im String",
|
|||
|
|
example_output="24.3",
|
|||
|
|
minimum_data_requirements="Profil mit height > 0 und mindestens ein weight_log",
|
|||
|
|
quality_filter_policy=None,
|
|||
|
|
confidence_logic="high nur wenn BMI berechenbar; sonst insufficient / Anzeige nicht verfügbar",
|
|||
|
|
missing_value_policy=MissingValuePolicy(
|
|||
|
|
available=False,
|
|||
|
|
value_raw=None,
|
|||
|
|
missing_reason="no_data",
|
|||
|
|
legacy_display="nicht verfügbar",
|
|||
|
|
),
|
|||
|
|
known_limitations=(
|
|||
|
|
"Keine ethnischen Referenzkurven; Profilgröße kann veraltet sein. "
|
|||
|
|
"Unterscheidet nicht Muskelmasse vs. Fett."
|
|||
|
|
),
|
|||
|
|
layer_1_decision="body_metrics.get_bmi_data",
|
|||
|
|
layer_2a_decision="placeholder_resolver.calculate_bmi (Format)",
|
|||
|
|
layer_2b_reuse_possible=True,
|
|||
|
|
architecture_alignment="Phase 0c",
|
|||
|
|
issue_53_alignment="Layer 1 als Quelle",
|
|||
|
|
evidence={},
|
|||
|
|
)
|
|||
|
|
for field in (
|
|||
|
|
"key", "category", "description", "resolver_module", "resolver_function",
|
|||
|
|
"data_layer_module", "data_layer_function", "source_tables",
|
|||
|
|
"semantic_contract", "business_meaning", "unit", "time_window",
|
|||
|
|
"output_type", "placeholder_type", "format_hint", "example_output",
|
|||
|
|
"minimum_data_requirements", "confidence_logic", "missing_value_policy",
|
|||
|
|
"known_limitations", "layer_1_decision", "layer_2a_decision",
|
|||
|
|
"layer_2b_reuse_possible", "architecture_alignment", "issue_53_alignment",
|
|||
|
|
):
|
|||
|
|
bmi.set_evidence(field, EvidenceType.CODE_DERIVED)
|
|||
|
|
bmi.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED)
|
|||
|
|
bmi.set_evidence("known_limitations", EvidenceType.MIXED)
|
|||
|
|
register_placeholder(bmi)
|
|||
|
|
|
|||
|
|
gw = PlaceholderMetadata(
|
|||
|
|
key="goal_weight",
|
|||
|
|
category="Körper",
|
|||
|
|
description="Zielgewicht aus Profilfeld profiles.goal_weight (kg)",
|
|||
|
|
resolver_module="backend/placeholder_resolver.py",
|
|||
|
|
resolver_function="get_goal_weight",
|
|||
|
|
data_layer_module="backend/data_layer/body_metrics.py",
|
|||
|
|
data_layer_function="get_profile_goal_weight_data",
|
|||
|
|
source_tables=["profiles"],
|
|||
|
|
semantic_contract=(
|
|||
|
|
"Strategisches Soll-Gewicht im Profil; unabhängig von der goals-Tabelle "
|
|||
|
|
"(dort detaillierte Ziele mit Fortschritt)."
|
|||
|
|
),
|
|||
|
|
business_meaning="Schneller Abgleich Prompt vs. Profil-Default-Zielgewicht",
|
|||
|
|
unit="kg",
|
|||
|
|
time_window="Profil-Snapshot",
|
|||
|
|
output_type=OutputType.NUMERIC,
|
|||
|
|
placeholder_type=PlaceholderType.RAW_DATA,
|
|||
|
|
format_hint="Eine Dezimalstelle oder Text „nicht gesetzt“",
|
|||
|
|
example_output="82.0",
|
|||
|
|
minimum_data_requirements="profiles.goal_weight IS NOT NULL",
|
|||
|
|
quality_filter_policy=None,
|
|||
|
|
confidence_logic="high wenn gesetzt",
|
|||
|
|
missing_value_policy=MissingValuePolicy(
|
|||
|
|
available=False,
|
|||
|
|
value_raw=None,
|
|||
|
|
missing_reason="not_set",
|
|||
|
|
legacy_display="nicht gesetzt",
|
|||
|
|
),
|
|||
|
|
known_limitations="Kann von aktiven goals.weight-Zielen abweichen.",
|
|||
|
|
layer_1_decision="body_metrics.get_profile_goal_weight_data",
|
|||
|
|
layer_2a_decision="placeholder_resolver.get_goal_weight",
|
|||
|
|
layer_2b_reuse_possible=True,
|
|||
|
|
architecture_alignment="Phase 0c",
|
|||
|
|
issue_53_alignment="Layer 1 als Quelle",
|
|||
|
|
evidence={},
|
|||
|
|
)
|
|||
|
|
for field in (
|
|||
|
|
"key", "category", "description", "resolver_module", "resolver_function",
|
|||
|
|
"data_layer_module", "data_layer_function", "source_tables",
|
|||
|
|
"semantic_contract", "unit", "time_window", "output_type",
|
|||
|
|
"placeholder_type", "format_hint", "example_output",
|
|||
|
|
"minimum_data_requirements", "confidence_logic", "missing_value_policy",
|
|||
|
|
"layer_1_decision", "layer_2a_decision", "layer_2b_reuse_possible",
|
|||
|
|
"architecture_alignment", "issue_53_alignment",
|
|||
|
|
):
|
|||
|
|
gw.set_evidence(field, EvidenceType.CODE_DERIVED)
|
|||
|
|
gw.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED)
|
|||
|
|
gw.set_evidence("known_limitations", EvidenceType.MIXED)
|
|||
|
|
register_placeholder(gw)
|
|||
|
|
|
|||
|
|
gbf = PlaceholderMetadata(
|
|||
|
|
key="goal_bf_pct",
|
|||
|
|
category="Körper",
|
|||
|
|
description="Ziel-Körperfettanteil aus Profilfeld profiles.goal_bf_pct (%)",
|
|||
|
|
resolver_module="backend/placeholder_resolver.py",
|
|||
|
|
resolver_function="get_goal_bf_pct",
|
|||
|
|
data_layer_module="backend/data_layer/body_metrics.py",
|
|||
|
|
data_layer_function="get_profile_goal_bf_pct_data",
|
|||
|
|
source_tables=["profiles"],
|
|||
|
|
semantic_contract="Strategisches Ziel-KFA im Profil.",
|
|||
|
|
business_meaning="Prompt-Abgleich mit Profil-Ziel-KFA",
|
|||
|
|
unit="%",
|
|||
|
|
time_window="Profil-Snapshot",
|
|||
|
|
output_type=OutputType.NUMERIC,
|
|||
|
|
placeholder_type=PlaceholderType.RAW_DATA,
|
|||
|
|
format_hint="Eine Dezimalstelle oder Text „nicht gesetzt“",
|
|||
|
|
example_output="15.0",
|
|||
|
|
minimum_data_requirements="profiles.goal_bf_pct IS NOT NULL",
|
|||
|
|
quality_filter_policy=None,
|
|||
|
|
confidence_logic="high wenn gesetzt",
|
|||
|
|
missing_value_policy=MissingValuePolicy(
|
|||
|
|
available=False,
|
|||
|
|
value_raw=None,
|
|||
|
|
missing_reason="not_set",
|
|||
|
|
legacy_display="nicht gesetzt",
|
|||
|
|
),
|
|||
|
|
known_limitations="Kann von goals body_fat abweichen.",
|
|||
|
|
layer_1_decision="body_metrics.get_profile_goal_bf_pct_data",
|
|||
|
|
layer_2a_decision="placeholder_resolver.get_goal_bf_pct",
|
|||
|
|
layer_2b_reuse_possible=True,
|
|||
|
|
architecture_alignment="Phase 0c",
|
|||
|
|
issue_53_alignment="Layer 1 als Quelle",
|
|||
|
|
evidence={},
|
|||
|
|
)
|
|||
|
|
for field in (
|
|||
|
|
"key", "category", "description", "resolver_module", "resolver_function",
|
|||
|
|
"data_layer_module", "data_layer_function", "source_tables",
|
|||
|
|
"semantic_contract", "unit", "time_window", "output_type",
|
|||
|
|
"placeholder_type", "format_hint", "example_output",
|
|||
|
|
"minimum_data_requirements", "confidence_logic", "missing_value_policy",
|
|||
|
|
"layer_1_decision", "layer_2a_decision", "layer_2b_reuse_possible",
|
|||
|
|
"architecture_alignment", "issue_53_alignment",
|
|||
|
|
):
|
|||
|
|
gbf.set_evidence(field, EvidenceType.CODE_DERIVED)
|
|||
|
|
gbf.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED)
|
|||
|
|
gbf.set_evidence("known_limitations", EvidenceType.MIXED)
|
|||
|
|
register_placeholder(gbf)
|
|||
|
|
|
|||
|
|
bps = PlaceholderMetadata(
|
|||
|
|
key="body_progress_score",
|
|||
|
|
category="Körper",
|
|||
|
|
description="Körper-Fortschritts-Score 0–100, gewichtet nach Focus (Abnehmen, Muskelaufbau, Recomp)",
|
|||
|
|
resolver_module="backend/placeholder_resolver.py",
|
|||
|
|
resolver_function="_safe_int",
|
|||
|
|
data_layer_module="backend/data_layer/body_metrics.py",
|
|||
|
|
data_layer_function="calculate_body_progress_score",
|
|||
|
|
source_tables=[
|
|||
|
|
"user_focus_area_weights",
|
|||
|
|
"focus_area_definitions",
|
|||
|
|
"goals",
|
|||
|
|
"weight_log",
|
|||
|
|
"caliper_log",
|
|||
|
|
"circumference_log",
|
|||
|
|
],
|
|||
|
|
semantic_contract=(
|
|||
|
|
"Gewichteter Mittelwert aus bis zu drei Komponenten: Trend vs. Gewichtsziel, "
|
|||
|
|
"Körperzusammensetzung (FM/LBM/Recomp-Quadrant), Taille-Trend. "
|
|||
|
|
"Komponenten nur aktiv, wenn passende Focus-Gewichte > 0."
|
|||
|
|
),
|
|||
|
|
business_meaning="Meta-KPI: passt dokumentierter Körperfortschritt zur gewichteten Körper-Priorität?",
|
|||
|
|
unit="Score (0–100)",
|
|||
|
|
time_window="composite (u. a. 28d Deltas, Ziel-Fortschritt)",
|
|||
|
|
output_type=OutputType.NUMERIC,
|
|||
|
|
placeholder_type=PlaceholderType.SCORE,
|
|||
|
|
format_hint="Ganzzahl oder „nicht verfügbar“",
|
|||
|
|
example_output="72",
|
|||
|
|
minimum_data_requirements=(
|
|||
|
|
"Summe der Körper-Focus-Gewichte (weight_loss + muscle_gain + body_recomposition) > 0 "
|
|||
|
|
"und mindestens eine bewertbare Komponente mit Daten."
|
|||
|
|
),
|
|||
|
|
quality_filter_policy=None,
|
|||
|
|
confidence_logic="Kein separates Confidence-Feld; None wenn keine Körper-Gewichtung oder keine Teilscores.",
|
|||
|
|
missing_value_policy=MissingValuePolicy(
|
|||
|
|
available=False,
|
|||
|
|
value_raw=None,
|
|||
|
|
missing_reason="not_applicable",
|
|||
|
|
legacy_display="nicht verfügbar",
|
|||
|
|
),
|
|||
|
|
known_limitations=(
|
|||
|
|
"Abhängig von user_focus_area_weights und aktiven weight-goals für Gewichts-Teilscore. "
|
|||
|
|
"Taille-Score wird mit festem Basisgewicht 20+ eingemischt und kann dominieren."
|
|||
|
|
),
|
|||
|
|
layer_1_decision="body_metrics.calculate_body_progress_score",
|
|||
|
|
layer_2a_decision="placeholder_resolver._safe_int('body_progress_score', …)",
|
|||
|
|
layer_2b_reuse_possible=True,
|
|||
|
|
architecture_alignment="Phase 0c",
|
|||
|
|
issue_53_alignment="Layer 1 als Quelle",
|
|||
|
|
evidence={},
|
|||
|
|
)
|
|||
|
|
for field in (
|
|||
|
|
"key", "category", "description", "resolver_module", "resolver_function",
|
|||
|
|
"data_layer_module", "data_layer_function", "source_tables",
|
|||
|
|
"semantic_contract", "unit", "time_window", "output_type",
|
|||
|
|
"placeholder_type", "format_hint", "example_output",
|
|||
|
|
"minimum_data_requirements", "confidence_logic", "missing_value_policy",
|
|||
|
|
"layer_1_decision", "layer_2a_decision", "layer_2b_reuse_possible",
|
|||
|
|
"architecture_alignment", "issue_53_alignment",
|
|||
|
|
):
|
|||
|
|
bps.set_evidence(field, EvidenceType.CODE_DERIVED)
|
|||
|
|
bps.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED)
|
|||
|
|
bps.set_evidence("known_limitations", EvidenceType.MIXED)
|
|||
|
|
register_placeholder(bps)
|
|||
|
|
|
|||
|
|
|
|||
|
|
register_body_extras()
|