diff --git a/CLAUDE.md b/CLAUDE.md index dd3436c..4945c22 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,6 +27,14 @@ - Production Deploy nur nach expliziter Freigabe - Migration 001-999 Pattern einhalten +**Placeholder/Metrics (VERBINDLICH ab 2026-04-02):** +- ✅ **ALLE** neuen Placeholder/Metrics MÜSSEN über Registry Framework registriert werden +- ✅ Registrierung in `backend/placeholder_registrations/` +- ✅ Evidence-basiertes Tagging für alle Metadatenfelder (CODE_DERIVED, DRAFT_DERIVED, etc.) +- ✅ Single Source of Truth für Prompt-Injektion, GUI, Export, Validierung +- 📚 **Verbindliche Dokumentation:** `.claude/docs/technical/PLACEHOLDER_REGISTRY_FRAMEWORK.md` +- ❌ **VERBOTEN:** Hardcoded Metadaten, duplizierte Definitionen, Placeholder ohne Registry + ## Projekt-Übersicht **Mitai Jinkendo** (身体 Jinkendo) – selbst-gehostete PWA für Körper-Tracking mit KI-Auswertung. Teil der **Jinkendo**-App-Familie (人拳道). Domains: jinkendo.de / .com / .life diff --git a/backend/placeholder_registrations/__init__.py b/backend/placeholder_registrations/__init__.py index c94c6d7..588cd39 100644 --- a/backend/placeholder_registrations/__init__.py +++ b/backend/placeholder_registrations/__init__.py @@ -9,5 +9,6 @@ from . import nutrition_part_a from . import nutrition_part_b from . import nutrition_part_c from . import body_metrics +from . import activity_metrics -__all__ = ['nutrition_part_a', 'nutrition_part_b', 'nutrition_part_c', 'body_metrics'] +__all__ = ['nutrition_part_a', 'nutrition_part_b', 'nutrition_part_c', 'body_metrics', 'activity_metrics'] diff --git a/backend/placeholder_registrations/activity_metrics.py b/backend/placeholder_registrations/activity_metrics.py new file mode 100644 index 0000000..92d2287 --- /dev/null +++ b/backend/placeholder_registrations/activity_metrics.py @@ -0,0 +1,1112 @@ +""" +Activity Metrics Placeholder Registrations + +Registers all 17 activity-related placeholders in the central placeholder registry. + +Evidence-based metadata with clear tagging of source. + +Groups: +- Legacy Resolver (3): activity_summary, activity_detail, trainingstyp_verteilung +- Basic Metrics (7): training_minutes_week, training_frequency_7d, quality_sessions_pct, + proxy_internal_load_7d, monotony_score, strain_score, rest_day_compliance +- Advanced Metrics (7): ability_balance_*, vo2max_trend_28d, activity_score +""" + +from placeholder_registry import ( + PlaceholderMetadata, + MissingValuePolicy, + EvidenceType, + OutputType, + PlaceholderType, + register_placeholder +) + + +# ============================================================================= +# GRUPPE 1: Legacy Resolver (3 Placeholders) +# ============================================================================= + +def register_activity_group_1(): + """ + Register Group 1: Legacy Resolver placeholders. + + These use old resolver pattern (direct formatting in resolver, no data layer). + """ + + # ── activity_summary ────────────────────────────────────────────────────── + + activity_summary_metadata = PlaceholderMetadata( + key="activity_summary", + category="Aktivität", + description="Zusammenfassung der letzten 14 Tage Aktivität", + resolver_module="backend/placeholder_resolver.py", + resolver_function="_format_activity_summary", + data_layer_module=None, + data_layer_function=None, + source_tables=["activity_log", "training_types"], + semantic_contract=( + "Liefert eine kompakte textuelle Zusammenfassung der Trainingsaktivitäten " + "der letzten 14 Tage. Beinhaltet: Anzahl Einheiten, Gesamtdauer, " + "Trainingstypen-Verteilung (Top 3), durchschnittliche Dauer pro Einheit." + ), + business_meaning=( + "Schneller Überblick für KI-Prompts über aktuelle Trainingsaktivität. " + "Erlaubt Prompt-Autoren, Trainingsvolumen und -vielfalt auf einen Blick " + "zu erfassen ohne einzelne Datenpunkte zu konsumieren." + ), + unit="text", + time_window="14d", + output_type=OutputType.TEXT_SUMMARY, + placeholder_type=PlaceholderType.INTERPRETED, + format_hint="Mehrere Sätze, menschenlesbar, Deutsch", + example_output=( + "14 Einheiten (315 min gesamt). Top-Typen: Krafttraining (5x, 180 min), " + "Ausdauer (4x, 90 min), Mobilität (3x, 30 min). Ø Dauer: 22 min/Einheit." + ), + minimum_data_requirements=None, + quality_filter_policy=None, + confidence_logic=( + "Keine formale Confidence-Berechnung. Verfügbarkeit = mindestens 1 " + "activity_log Eintrag in 14d." + ), + missing_value_policy=MissingValuePolicy( + available=False, + value_raw=None, + missing_reason="no_data", + legacy_display="Keine Aktivitätsdaten" + ), + known_limitations=( + "OLD RESOLVER PATTERN: Keine Data Layer Funktion. Formatierung direkt " + "im Resolver. JOIN mit training_types für Typ-Namen. " + "CRITICAL: Keine Qualitätsprüfung - auch 1-Minuten-Einheiten werden gezählt." + ), + layer_1_decision="NONE - Old resolver pattern (direct SQL in resolver)", + layer_2a_decision="Placeholder Resolver (formatting + SQL query)", + layer_2b_reuse_possible=False, + architecture_alignment=( + "NOT ALIGNED with Phase 0c Multi-Layer Architecture. " + "Should be refactored to use data_layer function." + ), + issue_53_alignment="NOT ALIGNED - no layer separation" + ) + + activity_summary_metadata.set_evidence("key", EvidenceType.CODE_DERIVED) + activity_summary_metadata.set_evidence("category", EvidenceType.CODE_DERIVED) + activity_summary_metadata.set_evidence("description", EvidenceType.DRAFT_DERIVED) + activity_summary_metadata.set_evidence("resolver_module", EvidenceType.CODE_DERIVED) + activity_summary_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED) + activity_summary_metadata.set_evidence("data_layer_module", EvidenceType.CODE_DERIVED) + activity_summary_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED) + activity_summary_metadata.set_evidence("source_tables", EvidenceType.CODE_DERIVED) + activity_summary_metadata.set_evidence("semantic_contract", EvidenceType.MIXED) + activity_summary_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED) + activity_summary_metadata.set_evidence("unit", EvidenceType.CODE_DERIVED) + activity_summary_metadata.set_evidence("time_window", EvidenceType.CODE_DERIVED) + activity_summary_metadata.set_evidence("output_type", EvidenceType.CODE_DERIVED) + activity_summary_metadata.set_evidence("placeholder_type", EvidenceType.MIXED) + activity_summary_metadata.set_evidence("format_hint", EvidenceType.CODE_DERIVED) + activity_summary_metadata.set_evidence("example_output", EvidenceType.CODE_DERIVED) + activity_summary_metadata.set_evidence("minimum_data_requirements", EvidenceType.UNRESOLVED) + activity_summary_metadata.set_evidence("quality_filter_policy", EvidenceType.UNRESOLVED) + activity_summary_metadata.set_evidence("confidence_logic", EvidenceType.CODE_DERIVED) + activity_summary_metadata.set_evidence("missing_value_policy", EvidenceType.CODE_DERIVED) + activity_summary_metadata.set_evidence("known_limitations", EvidenceType.CODE_DERIVED) + activity_summary_metadata.set_evidence("layer_1_decision", EvidenceType.CODE_DERIVED) + activity_summary_metadata.set_evidence("layer_2a_decision", EvidenceType.CODE_DERIVED) + activity_summary_metadata.set_evidence("layer_2b_reuse_possible", EvidenceType.CODE_DERIVED) + activity_summary_metadata.set_evidence("architecture_alignment", EvidenceType.CODE_DERIVED) + activity_summary_metadata.set_evidence("issue_53_alignment", EvidenceType.CODE_DERIVED) + + register_placeholder(activity_summary_metadata) + + # ── activity_detail ─────────────────────────────────────────────────────── + + activity_detail_metadata = PlaceholderMetadata( + key="activity_detail", + category="Aktivität", + description="Detaillierte Liste der letzten 14 Tage Aktivität", + resolver_module="backend/placeholder_resolver.py", + resolver_function="_format_activity_detail", + data_layer_module=None, + data_layer_function=None, + source_tables=["activity_log", "training_types"], + semantic_contract=( + "Liefert eine strukturierte Liste aller Trainingseinheiten der letzten 14 Tage. " + "Jede Einheit: Datum, Trainingstyp, Dauer (Minuten), optional Notizen. " + "Sortiert chronologisch absteigend (neueste zuerst)." + ), + business_meaning=( + "Detaillierte Trainingshistorie für KI-Prompts, die Muster, Progressionen " + "oder Abweichungen analysieren sollen. Erlaubt Erkennung von Trainingszyklen, " + "Pausentagen, Intensitätsmustern." + ), + unit="list", + time_window="14d", + output_type=OutputType.LIST, + placeholder_type=PlaceholderType.RAW_DATA, + format_hint="Liste von Strings, eine Zeile pro Einheit: 'YYYY-MM-DD: Typ (Dauer min)'", + example_output=( + "2026-03-28: Krafttraining (45 min)\\n" + "2026-03-27: Laufen (30 min)\\n" + "2026-03-25: Yoga (20 min)" + ), + minimum_data_requirements=None, + quality_filter_policy=None, + confidence_logic="Keine Confidence-Berechnung. Rohdaten-Liste.", + missing_value_policy=MissingValuePolicy( + available=False, + value_raw=None, + missing_reason="no_data", + legacy_display="Keine Aktivitätsdaten" + ), + known_limitations=( + "OLD RESOLVER PATTERN: Keine Data Layer Funktion. " + "Formatierung direkt im Resolver. " + "CRITICAL: Keine Qualitätsfilterung - auch ungültige Einheiten (z.B. 0 min) " + "werden gelistet. JOIN mit training_types für Typ-Namen." + ), + layer_1_decision="NONE - Old resolver pattern (direct SQL in resolver)", + layer_2a_decision="Placeholder Resolver (formatting + SQL query)", + layer_2b_reuse_possible=False, + architecture_alignment=( + "NOT ALIGNED with Phase 0c Multi-Layer Architecture. " + "Should be refactored to use data_layer function." + ), + issue_53_alignment="NOT ALIGNED - no layer separation" + ) + + activity_detail_metadata.set_evidence("key", EvidenceType.CODE_DERIVED) + activity_detail_metadata.set_evidence("category", EvidenceType.CODE_DERIVED) + activity_detail_metadata.set_evidence("description", EvidenceType.DRAFT_DERIVED) + activity_detail_metadata.set_evidence("resolver_module", EvidenceType.CODE_DERIVED) + activity_detail_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED) + activity_detail_metadata.set_evidence("data_layer_module", EvidenceType.CODE_DERIVED) + activity_detail_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED) + activity_detail_metadata.set_evidence("source_tables", EvidenceType.CODE_DERIVED) + activity_detail_metadata.set_evidence("semantic_contract", EvidenceType.MIXED) + activity_detail_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED) + activity_detail_metadata.set_evidence("unit", EvidenceType.CODE_DERIVED) + activity_detail_metadata.set_evidence("time_window", EvidenceType.CODE_DERIVED) + activity_detail_metadata.set_evidence("output_type", EvidenceType.CODE_DERIVED) + activity_detail_metadata.set_evidence("placeholder_type", EvidenceType.MIXED) + activity_detail_metadata.set_evidence("format_hint", EvidenceType.CODE_DERIVED) + activity_detail_metadata.set_evidence("example_output", EvidenceType.CODE_DERIVED) + activity_detail_metadata.set_evidence("minimum_data_requirements", EvidenceType.UNRESOLVED) + activity_detail_metadata.set_evidence("quality_filter_policy", EvidenceType.UNRESOLVED) + activity_detail_metadata.set_evidence("confidence_logic", EvidenceType.CODE_DERIVED) + activity_detail_metadata.set_evidence("missing_value_policy", EvidenceType.CODE_DERIVED) + activity_detail_metadata.set_evidence("known_limitations", EvidenceType.CODE_DERIVED) + activity_detail_metadata.set_evidence("layer_1_decision", EvidenceType.CODE_DERIVED) + activity_detail_metadata.set_evidence("layer_2a_decision", EvidenceType.CODE_DERIVED) + activity_detail_metadata.set_evidence("layer_2b_reuse_possible", EvidenceType.CODE_DERIVED) + activity_detail_metadata.set_evidence("architecture_alignment", EvidenceType.CODE_DERIVED) + activity_detail_metadata.set_evidence("issue_53_alignment", EvidenceType.CODE_DERIVED) + + register_placeholder(activity_detail_metadata) + + # ── trainingstyp_verteilung ─────────────────────────────────────────────── + + trainingstyp_verteilung_metadata = PlaceholderMetadata( + key="trainingstyp_verteilung", + category="Aktivität", + description="Trainingstypen-Verteilung der letzten 14 Tage als JSON", + resolver_module="backend/placeholder_resolver.py", + resolver_function="_format_trainingstyp_verteilung", + data_layer_module=None, + data_layer_function=None, + source_tables=["activity_log", "training_types"], + semantic_contract=( + "Liefert eine JSON-Struktur mit der Verteilung der Trainingstypen über 14 Tage. " + "Für jeden Trainingstyp: Anzahl Einheiten, Gesamtdauer (Minuten), " + "Prozentanteil an Gesamtdauer. Sortiert nach Dauer absteigend." + ), + business_meaning=( + "Analyse-Placeholder für Trainingsvielfalt und -schwerpunkte. " + "Erlaubt KI-Prompts, Imbalancen zu erkennen (z.B. nur Kraft, keine Ausdauer) " + "oder Zielkonformität zu prüfen (z.B. 'zu wenig Mobilität')." + ), + unit="json", + time_window="14d", + output_type=OutputType.JSON, + placeholder_type=PlaceholderType.INTERPRETED, + format_hint="JSON Object mit Trainingstyp als Key, Value: {count, duration_min, percentage}", + example_output=( + '{"Krafttraining": {"count": 5, "duration_min": 180, "percentage": 57}, ' + '"Ausdauer": {"count": 4, "duration_min": 90, "percentage": 29}, ' + '"Mobilität": {"count": 3, "duration_min": 45, "percentage": 14}}' + ), + minimum_data_requirements=None, + quality_filter_policy=None, + confidence_logic="Keine Confidence-Berechnung. Aggregation basiert auf verfügbaren Daten.", + missing_value_policy=MissingValuePolicy( + available=False, + value_raw=None, + missing_reason="no_data", + legacy_display="{}" + ), + known_limitations=( + "OLD RESOLVER PATTERN: Keine Data Layer Funktion. " + "Aggregation direkt im Resolver. " + "CRITICAL: Keine Qualitätsfilterung - auch ungültige Einheiten werden aggregiert. " + "JOIN mit training_types für Typ-Namen. " + "EDGE CASE: Einheiten ohne training_type_id werden ignoriert (LEFT JOIN)." + ), + layer_1_decision="NONE - Old resolver pattern (direct SQL aggregation in resolver)", + layer_2a_decision="Placeholder Resolver (aggregation + JSON formatting)", + layer_2b_reuse_possible=True, + architecture_alignment=( + "PARTIALLY ALIGNED: JSON output structure suitable for chart endpoints, " + "but no data layer separation. Should be refactored." + ), + issue_53_alignment="PARTIALLY ALIGNED - output format good, layer separation missing" + ) + + trainingstyp_verteilung_metadata.set_evidence("key", EvidenceType.CODE_DERIVED) + trainingstyp_verteilung_metadata.set_evidence("category", EvidenceType.CODE_DERIVED) + trainingstyp_verteilung_metadata.set_evidence("description", EvidenceType.DRAFT_DERIVED) + trainingstyp_verteilung_metadata.set_evidence("resolver_module", EvidenceType.CODE_DERIVED) + trainingstyp_verteilung_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED) + trainingstyp_verteilung_metadata.set_evidence("data_layer_module", EvidenceType.CODE_DERIVED) + trainingstyp_verteilung_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED) + trainingstyp_verteilung_metadata.set_evidence("source_tables", EvidenceType.CODE_DERIVED) + trainingstyp_verteilung_metadata.set_evidence("semantic_contract", EvidenceType.MIXED) + trainingstyp_verteilung_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED) + trainingstyp_verteilung_metadata.set_evidence("unit", EvidenceType.CODE_DERIVED) + trainingstyp_verteilung_metadata.set_evidence("time_window", EvidenceType.CODE_DERIVED) + trainingstyp_verteilung_metadata.set_evidence("output_type", EvidenceType.CODE_DERIVED) + trainingstyp_verteilung_metadata.set_evidence("placeholder_type", EvidenceType.MIXED) + trainingstyp_verteilung_metadata.set_evidence("format_hint", EvidenceType.CODE_DERIVED) + trainingstyp_verteilung_metadata.set_evidence("example_output", EvidenceType.CODE_DERIVED) + trainingstyp_verteilung_metadata.set_evidence("minimum_data_requirements", EvidenceType.UNRESOLVED) + trainingstyp_verteilung_metadata.set_evidence("quality_filter_policy", EvidenceType.UNRESOLVED) + trainingstyp_verteilung_metadata.set_evidence("confidence_logic", EvidenceType.CODE_DERIVED) + trainingstyp_verteilung_metadata.set_evidence("missing_value_policy", EvidenceType.CODE_DERIVED) + trainingstyp_verteilung_metadata.set_evidence("known_limitations", EvidenceType.CODE_DERIVED) + trainingstyp_verteilung_metadata.set_evidence("layer_1_decision", EvidenceType.CODE_DERIVED) + trainingstyp_verteilung_metadata.set_evidence("layer_2a_decision", EvidenceType.CODE_DERIVED) + trainingstyp_verteilung_metadata.set_evidence("layer_2b_reuse_possible", EvidenceType.TO_VERIFY) + trainingstyp_verteilung_metadata.set_evidence("architecture_alignment", EvidenceType.MIXED) + trainingstyp_verteilung_metadata.set_evidence("issue_53_alignment", EvidenceType.MIXED) + + register_placeholder(trainingstyp_verteilung_metadata) + + +# ============================================================================= +# GRUPPE 2: Basic Metrics (7 Placeholders) +# ============================================================================= + +def register_activity_group_2(): + """ + Register Group 2: Basic Activity Metrics. + + All use Phase 0c Data Layer architecture. + """ + + # Common metadata for most Group 2 placeholders + common_metadata_7d = { + "category": "Aktivität", + "resolver_module": "backend/placeholder_resolver.py", + "data_layer_module": "backend/data_layer/activity_metrics.py", + "source_tables": ["activity_log"], + "time_window": "7d", + "output_type": OutputType.NUMERIC, + "placeholder_type": PlaceholderType.INTERPRETED, + "confidence_logic": "datenpunktbasierte Coverage-Logik (calculate_confidence in data layer)", + "missing_value_policy": MissingValuePolicy( + available=False, + value_raw=None, + missing_reason="insufficient_data", + legacy_display="nicht genug Daten" + ), + "layer_1_decision": "Data Layer (activity_metrics.py)", + "layer_2a_decision": "Placeholder Resolver (formatting only)", + "architecture_alignment": "Phase 0c Multi-Layer Architecture conform", + "issue_53_alignment": "Layer separation established" + } + + # Common evidence for Group 2 placeholders + common_evidence_7d = { + "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, + "confidence_logic": EvidenceType.CODE_DERIVED, + "missing_value_policy": EvidenceType.CODE_DERIVED, + "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, + "minimum_data_requirements": EvidenceType.UNRESOLVED, + "quality_filter_policy": EvidenceType.UNRESOLVED + } + + # ── training_minutes_week ───────────────────────────────────────────────── + + training_minutes_week_metadata = PlaceholderMetadata( + key="training_minutes_week", + description="Gesamte Trainingsminuten in 7 Tagen", + resolver_function="get_training_minutes_week", + data_layer_function="get_weekly_training_volume", + semantic_contract=( + "Liefert die Summe aller Trainingsminuten über die letzten 7 Tage. " + "Basiert auf activity_log.duration_minutes. Keine Qualitätsfilterung - " + "alle Einheiten werden summiert unabhängig von Dauer oder Intensität." + ), + business_meaning=( + "Kernmetrik für Trainingsvolumen und Periodisierung. " + "Erlaubt Erkennung von Übertraining (zu hoch), Detraining (zu niedrig), " + "und Trendanalysen (Volumen steigend/fallend)." + ), + unit="Minuten", + format_hint="Ganzzahl", + example_output="315", + known_limitations=( + "Keine Qualitätsfilterung. 1-Minuten-Einheiten zählen gleich wie " + "60-Minuten-Sessions. Keine Intensitätsgewichtung. " + "EDGE CASE: duration_minutes = 0 oder NULL werden als 0 gezählt." + ), + layer_2b_reuse_possible=True, + **common_metadata_7d + ) + + training_minutes_week_metadata.evidence.update(common_evidence_7d) + training_minutes_week_metadata.set_evidence("key", EvidenceType.CODE_DERIVED) + training_minutes_week_metadata.set_evidence("description", EvidenceType.DRAFT_DERIVED) + training_minutes_week_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED) + training_minutes_week_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED) + training_minutes_week_metadata.set_evidence("semantic_contract", EvidenceType.MIXED) + training_minutes_week_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED) + training_minutes_week_metadata.set_evidence("unit", EvidenceType.CODE_DERIVED) + training_minutes_week_metadata.set_evidence("format_hint", EvidenceType.CODE_DERIVED) + training_minutes_week_metadata.set_evidence("example_output", EvidenceType.CODE_DERIVED) + training_minutes_week_metadata.set_evidence("known_limitations", EvidenceType.CODE_DERIVED) + + register_placeholder(training_minutes_week_metadata) + + # ── training_frequency_7d ───────────────────────────────────────────────── + + training_frequency_7d_metadata = PlaceholderMetadata( + key="training_frequency_7d", + description="Anzahl Trainingseinheiten in 7 Tagen", + resolver_function="get_training_frequency_7d", + data_layer_function="get_training_frequency", + semantic_contract=( + "Liefert die Anzahl der Trainingseinheiten über die letzten 7 Tage. " + "Basiert auf COUNT(activity_log.id). Keine Qualitätsfilterung - " + "jede Einheit wird gezählt unabhängig von Dauer oder Intensität." + ), + business_meaning=( + "Frequenz-Metrik für Trainingsregelmäßigkeit. " + "Erlaubt Erkennung von Konsistenz (z.B. 4-5 Einheiten/Woche), " + "Pausentagen, und Trainingszyklen." + ), + unit="Anzahl Einheiten", + format_hint="Ganzzahl", + example_output="5", + known_limitations=( + "Keine Qualitätsfilterung. 1-Minuten-Einheiten zählen gleich wie " + "60-Minuten-Sessions. " + "EDGE CASE: Mehrere Einheiten am selben Tag werden separat gezählt " + "(kein Tages-Grouping)." + ), + layer_2b_reuse_possible=True, + **common_metadata_7d + ) + + training_frequency_7d_metadata.evidence.update(common_evidence_7d) + training_frequency_7d_metadata.set_evidence("key", EvidenceType.CODE_DERIVED) + training_frequency_7d_metadata.set_evidence("description", EvidenceType.DRAFT_DERIVED) + training_frequency_7d_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED) + training_frequency_7d_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED) + training_frequency_7d_metadata.set_evidence("semantic_contract", EvidenceType.MIXED) + training_frequency_7d_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED) + training_frequency_7d_metadata.set_evidence("unit", EvidenceType.CODE_DERIVED) + training_frequency_7d_metadata.set_evidence("format_hint", EvidenceType.CODE_DERIVED) + training_frequency_7d_metadata.set_evidence("example_output", EvidenceType.CODE_DERIVED) + training_frequency_7d_metadata.set_evidence("known_limitations", EvidenceType.CODE_DERIVED) + + register_placeholder(training_frequency_7d_metadata) + + # ── quality_sessions_pct ────────────────────────────────────────────────── + + # Custom metadata for quality_sessions_pct (28d time window, different source tables) + quality_metadata_custom = {**common_metadata_7d} + quality_metadata_custom["time_window"] = "28d" + quality_metadata_custom["source_tables"] = ["activity_log", "training_types"] + + quality_sessions_pct_metadata = PlaceholderMetadata( + key="quality_sessions_pct", + description="Prozentsatz Qualitäts-Sessions in 28 Tagen", + resolver_function="get_quality_sessions_pct", + data_layer_function="calculate_quality_sessions_percentage", + semantic_contract=( + "Liefert den Prozentsatz der Trainingseinheiten, die als 'Qualitäts-Sessions' " + "klassifiziert werden (duration >= 30 min UND intensity >= moderate). " + "Berechnung: (quality_sessions / total_sessions) × 100." + ), + business_meaning=( + "Qualitäts-Metrik für Trainingseffektivität. " + "Niedrige Werte (<50%) deuten auf zu viele kurze/low-intensity Sessions. " + "Hohe Werte (>80%) deuten auf konsequent forderndes Training." + ), + unit="Prozent", + format_hint="Ganzzahl 0-100", + example_output="68", + known_limitations=( + "CRITICAL TO_VERIFY: Code nutzt quality_label Feld (may not exist). " + "Canonical expects duration >= 30min + intensity >= moderate. " + "MISMATCH: quality_label vs. calculated quality. " + "EDGE CASE: Einheiten ohne training_type_id werden ignoriert." + ), + layer_2b_reuse_possible=True, + **quality_metadata_custom + ) + + quality_sessions_pct_metadata.evidence.update(common_evidence_7d) + quality_sessions_pct_metadata.evidence["time_window"] = EvidenceType.CODE_DERIVED + quality_sessions_pct_metadata.set_evidence("key", EvidenceType.CODE_DERIVED) + quality_sessions_pct_metadata.set_evidence("description", EvidenceType.DRAFT_DERIVED) + quality_sessions_pct_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED) + quality_sessions_pct_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED) + quality_sessions_pct_metadata.set_evidence("semantic_contract", EvidenceType.TO_VERIFY) + quality_sessions_pct_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED) + quality_sessions_pct_metadata.set_evidence("unit", EvidenceType.CODE_DERIVED) + quality_sessions_pct_metadata.set_evidence("format_hint", EvidenceType.CODE_DERIVED) + quality_sessions_pct_metadata.set_evidence("example_output", EvidenceType.CODE_DERIVED) + quality_sessions_pct_metadata.set_evidence("known_limitations", EvidenceType.CODE_DERIVED) + quality_sessions_pct_metadata.set_evidence("source_tables", EvidenceType.CODE_DERIVED) + + register_placeholder(quality_sessions_pct_metadata) + + # ── proxy_internal_load_7d ──────────────────────────────────────────────── + + # Custom metadata for proxy_internal_load (different source tables) + proxy_load_metadata_custom = {**common_metadata_7d} + proxy_load_metadata_custom["source_tables"] = ["activity_log", "training_types"] + + proxy_internal_load_7d_metadata = PlaceholderMetadata( + key="proxy_internal_load_7d", + description="Proxy Internal Load über 7 Tage", + resolver_function="get_proxy_internal_load_7d", + data_layer_function="calculate_proxy_internal_load", + semantic_contract=( + "Liefert einen Proxy-Wert für die interne Trainingsbelastung über 7 Tage. " + "Berechnet als: Summe(duration × intensity_factor × quality_factor). " + "Intensity_factor basiert auf RPE-Mapping (1-10 → 0.1-1.0). " + "Quality_factor basiert auf quality_label (easy/moderate/hard → 0.8/1.0/1.2)." + ), + business_meaning=( + "Load-Monitoring-Metrik für Periodisierung und Übertrainings-Prävention. " + "Erlaubt Vergleich interner Belastung über Zeit (nicht nur Volumen). " + "Basis für Monotony und Strain Scores." + ), + unit="Load-Punkte", + format_hint="Ganzzahl", + example_output="1850", + known_limitations=( + "PROXY-WERT: Nicht trainings wissenschaftlich validiert. " + "RPE-Mapping vereinfacht (linear 1-10 → 0.1-1.0). " + "CRITICAL BUG: RPE 4-5 maps to 'moderate' quality but 'moderate' " + "not in quality_factors dict (easy/hard only). " + "EDGE CASE: Einheiten ohne RPE bekommen intensity_factor = 0.5 (default)." + ), + layer_2b_reuse_possible=True, + **proxy_load_metadata_custom + ) + + proxy_internal_load_7d_metadata.evidence.update(common_evidence_7d) + proxy_internal_load_7d_metadata.set_evidence("key", EvidenceType.CODE_DERIVED) + proxy_internal_load_7d_metadata.set_evidence("description", EvidenceType.DRAFT_DERIVED) + proxy_internal_load_7d_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED) + proxy_internal_load_7d_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED) + proxy_internal_load_7d_metadata.set_evidence("semantic_contract", EvidenceType.CODE_DERIVED) + proxy_internal_load_7d_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED) + proxy_internal_load_7d_metadata.set_evidence("unit", EvidenceType.DRAFT_DERIVED) + proxy_internal_load_7d_metadata.set_evidence("format_hint", EvidenceType.CODE_DERIVED) + proxy_internal_load_7d_metadata.set_evidence("example_output", EvidenceType.CODE_DERIVED) + proxy_internal_load_7d_metadata.set_evidence("known_limitations", EvidenceType.CODE_DERIVED) + proxy_internal_load_7d_metadata.set_evidence("source_tables", EvidenceType.CODE_DERIVED) + + register_placeholder(proxy_internal_load_7d_metadata) + + # ── monotony_score ──────────────────────────────────────────────────────── + + monotony_score_metadata = PlaceholderMetadata( + key="monotony_score", + description="Trainings-Monotonie-Score über 7 Tage", + resolver_function="get_monotony_score", + data_layer_function="calculate_monotony_score", + semantic_contract=( + "Liefert einen Monotonie-Score für die Trainingsbelastung über 7 Tage. " + "Berechnet als: mean(daily_duration) / std_dev(daily_duration). " + "Hohe Werte (>2.0) = monotones Training (gleichbleibende Belastung). " + "Niedrige Werte (<1.5) = variable Belastung." + ), + business_meaning=( + "Variabilitäts-Metrik für Periodisierung. " + "Hohe Monotonie erhöht Übertrainings-Risiko (Foster et al. 2001). " + "Kombination mit Strain Score für Übertrainings-Warnung." + ), + unit="Score (dimensionslos)", + format_hint="Dezimalzahl, typisch 0.5-3.0", + example_output="1.8", + known_limitations=( + "FORMULA: mean / std_dev. " + "EDGE CASE: std_dev = 0 (alle Tage gleich) → division by zero → return mean as fallback. " + "EDGE CASE: < 3 Trainingstage → nicht aussagekräftig, aber wird trotzdem berechnet. " + "ZEITFENSTER: 7 Tage möglicherweise zu kurz für verlässliche Monotonie-Analyse " + "(Foster original: 28 Tage)." + ), + layer_2b_reuse_possible=True, + **common_metadata_7d + ) + + monotony_score_metadata.evidence.update(common_evidence_7d) + monotony_score_metadata.set_evidence("key", EvidenceType.CODE_DERIVED) + monotony_score_metadata.set_evidence("description", EvidenceType.DRAFT_DERIVED) + monotony_score_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED) + monotony_score_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED) + monotony_score_metadata.set_evidence("semantic_contract", EvidenceType.CODE_DERIVED) + monotony_score_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED) + monotony_score_metadata.set_evidence("unit", EvidenceType.DRAFT_DERIVED) + monotony_score_metadata.set_evidence("format_hint", EvidenceType.CODE_DERIVED) + monotony_score_metadata.set_evidence("example_output", EvidenceType.CODE_DERIVED) + monotony_score_metadata.set_evidence("known_limitations", EvidenceType.CODE_DERIVED) + + register_placeholder(monotony_score_metadata) + + # ── strain_score ────────────────────────────────────────────────────────── + + strain_score_metadata = PlaceholderMetadata( + key="strain_score", + description="Trainings-Strain-Score über 7 Tage", + resolver_function="get_strain_score", + data_layer_function="calculate_strain_score", + semantic_contract=( + "Liefert einen Strain-Score für die Gesamtbelastung über 7 Tage. " + "Berechnet als: proxy_internal_load_7d × monotony_score. " + "Kombiniert Volumen (Load) mit Variabilität (Monotonie) zu Gesamt-Strain." + ), + business_meaning=( + "Übertrainings-Risiko-Metrik (Foster et al. 2001). " + "Hohe Werte = hohes Risiko (hohe Last + monotones Training). " + "Schwellenwerte: <5000 = moderat, 5000-8000 = erhöht, >8000 = kritisch." + ), + unit="Strain-Punkte", + format_hint="Ganzzahl, typisch 1000-10000", + example_output="3330", + known_limitations=( + "FORMULA: load × monotony. " + "ABHÄNGIG von proxy_internal_load_7d (bereits Proxy-Wert). " + "SCHWELLENWERTE: Nicht wissenschaftlich validiert für diese App. " + "EDGE CASE: Wenn monotony_score Fallback-Logik greift (std_dev=0), " + "wird Strain = Load × mean (nicht original Formel)." + ), + layer_2b_reuse_possible=True, + **common_metadata_7d + ) + + strain_score_metadata.evidence.update(common_evidence_7d) + strain_score_metadata.set_evidence("key", EvidenceType.CODE_DERIVED) + strain_score_metadata.set_evidence("description", EvidenceType.DRAFT_DERIVED) + strain_score_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED) + strain_score_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED) + strain_score_metadata.set_evidence("semantic_contract", EvidenceType.CODE_DERIVED) + strain_score_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED) + strain_score_metadata.set_evidence("unit", EvidenceType.DRAFT_DERIVED) + strain_score_metadata.set_evidence("format_hint", EvidenceType.DRAFT_DERIVED) + strain_score_metadata.set_evidence("example_output", EvidenceType.CODE_DERIVED) + strain_score_metadata.set_evidence("known_limitations", EvidenceType.CODE_DERIVED) + + register_placeholder(strain_score_metadata) + + # ── rest_day_compliance ─────────────────────────────────────────────────── + + # Custom metadata for rest_day_compliance (28d time window, different source tables) + rest_metadata_custom = {**common_metadata_7d} + rest_metadata_custom["time_window"] = "28d" + rest_metadata_custom["source_tables"] = ["rest_days", "activity_log"] + + rest_day_compliance_metadata = PlaceholderMetadata( + key="rest_day_compliance", + description="Ruhetag-Compliance über 28 Tage", + resolver_function="get_rest_day_compliance", + data_layer_function="calculate_rest_day_compliance", + semantic_contract=( + "Liefert den Prozentsatz der geplanten Ruhetage, die tatsächlich eingehalten wurden. " + "Berechnung: (eingehaltene_ruhetage / geplante_ruhetage) × 100. " + "Ruhetag = Eintrag in rest_days OHNE korrespondierenden activity_log Eintrag am selben Tag." + ), + business_meaning=( + "Regenerations-Compliance-Metrik. " + "Niedrige Werte (<70%) = schlechte Regeneration, Übertrainings-Risiko. " + "Hohe Werte (>90%) = gute Disziplin bei Regeneration." + ), + unit="Prozent", + format_hint="Ganzzahl 0-100", + example_output="85", + known_limitations=( + "JSONB DEPENDENCY: Nutzt rest_config->>'focus' JSONB Feld. " + "EDGE CASE: rest_config NULL → Ruhetag wird ignoriert (nicht gezählt). " + "EDGE CASE: Kein activity_log Eintrag am Ruhetag = compliant (korrekt). " + "EDGE CASE: Mehrere activity_log Einträge am Ruhetag = alle zählen als violation." + ), + layer_2b_reuse_possible=False, + **rest_metadata_custom + ) + + rest_day_compliance_metadata.evidence.update(common_evidence_7d) + rest_day_compliance_metadata.evidence["time_window"] = EvidenceType.CODE_DERIVED + rest_day_compliance_metadata.set_evidence("key", EvidenceType.CODE_DERIVED) + rest_day_compliance_metadata.set_evidence("description", EvidenceType.DRAFT_DERIVED) + rest_day_compliance_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED) + rest_day_compliance_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED) + rest_day_compliance_metadata.set_evidence("semantic_contract", EvidenceType.MIXED) + rest_day_compliance_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED) + rest_day_compliance_metadata.set_evidence("unit", EvidenceType.CODE_DERIVED) + rest_day_compliance_metadata.set_evidence("format_hint", EvidenceType.CODE_DERIVED) + rest_day_compliance_metadata.set_evidence("example_output", EvidenceType.CODE_DERIVED) + rest_day_compliance_metadata.set_evidence("known_limitations", EvidenceType.CODE_DERIVED) + rest_day_compliance_metadata.set_evidence("source_tables", EvidenceType.CODE_DERIVED) + rest_day_compliance_metadata.set_evidence("layer_2b_reuse_possible", EvidenceType.CODE_DERIVED) + + register_placeholder(rest_day_compliance_metadata) + + +# ============================================================================= +# GRUPPE 3: Advanced Metrics (7 Placeholders) +# ============================================================================= + +def register_activity_group_3(): + """ + Register Group 3: Advanced Activity Metrics. + + Includes ability balance metrics (5), VO2 Max trend, and activity score. + """ + + # Common metadata for ability_balance placeholders + common_metadata_ability = { + "category": "Aktivität", + "resolver_module": "backend/placeholder_resolver.py", + "data_layer_module": "backend/data_layer/activity_metrics.py", + "data_layer_function": "calculate_ability_balance", + "source_tables": ["activity_log", "training_types"], + "time_window": "28d", + "output_type": OutputType.NUMERIC, + "placeholder_type": PlaceholderType.SCORE, + "unit": "Score (0-100)", + "format_hint": "Ganzzahl 0-100", + "confidence_logic": "datenpunktbasierte Coverage-Logik (calculate_confidence in data layer)", + "missing_value_policy": MissingValuePolicy( + available=False, + value_raw=None, + missing_reason="insufficient_data", + legacy_display="nicht genug Daten" + ), + "layer_1_decision": "Data Layer (activity_metrics.calculate_ability_balance)", + "layer_2a_decision": "Placeholder Resolver (extract specific ability from dict)", + "architecture_alignment": "Phase 0c Multi-Layer Architecture conform", + "issue_53_alignment": "Layer separation established" + } + + # Common evidence for ability_balance placeholders + common_evidence_ability = { + "category": EvidenceType.CODE_DERIVED, + "resolver_module": EvidenceType.CODE_DERIVED, + "data_layer_module": EvidenceType.CODE_DERIVED, + "data_layer_function": EvidenceType.CODE_DERIVED, + "source_tables": EvidenceType.CODE_DERIVED, + "time_window": EvidenceType.CODE_DERIVED, + "output_type": EvidenceType.CODE_DERIVED, + "placeholder_type": EvidenceType.MIXED, + "unit": EvidenceType.CODE_DERIVED, + "format_hint": EvidenceType.CODE_DERIVED, + "confidence_logic": EvidenceType.CODE_DERIVED, + "missing_value_policy": EvidenceType.CODE_DERIVED, + "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, + "minimum_data_requirements": EvidenceType.UNRESOLVED, + "quality_filter_policy": EvidenceType.UNRESOLVED + } + + # ── ability_balance_strength ────────────────────────────────────────────── + + ability_balance_strength_metadata = PlaceholderMetadata( + key="ability_balance_strength", + description="Fähigkeiten-Balance-Score: Kraft (28d)", + resolver_function="get_ability_balance_strength", + semantic_contract=( + "Liefert einen normierten Score (0-100) für die relative Trainingsbelastung " + "der Fähigkeit 'Kraft' über 28 Tage. Berechnung: " + "(kraft_load / max_load_aller_fähigkeiten) × 100. " + "Basiert auf training_types.abilities JSONB Feld (strength_percentage)." + ), + business_meaning=( + "Balance-Metrik für Trainingsvielfalt. " + "Zeigt ob Kraft überproportional (>80), balanciert (40-60), oder unterrepräsentiert (<20) trainiert wird. " + "Vergleich mit anderen Fähigkeiten zeigt Imbalancen." + ), + example_output="75", + known_limitations=( + "JSONB DEPENDENCY: training_types.abilities JSONB Feld muss populated sein. " + "FORMULA: max-normalized (nicht prozentual vom Total). " + "Höchste Fähigkeit = 100, andere relativ dazu. " + "EDGE CASE: abilities NULL → Trainingstyp wird mit 0% für alle Fähigkeiten gewertet. " + "EDGE CASE: Alle Fähigkeiten = 0 (keine Trainings) → division by zero → return 0." + ), + layer_2b_reuse_possible=True, + **common_metadata_ability + ) + + ability_balance_strength_metadata.evidence.update(common_evidence_ability) + ability_balance_strength_metadata.set_evidence("key", EvidenceType.CODE_DERIVED) + ability_balance_strength_metadata.set_evidence("description", EvidenceType.DRAFT_DERIVED) + ability_balance_strength_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED) + ability_balance_strength_metadata.set_evidence("semantic_contract", EvidenceType.CODE_DERIVED) + ability_balance_strength_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED) + ability_balance_strength_metadata.set_evidence("example_output", EvidenceType.CODE_DERIVED) + ability_balance_strength_metadata.set_evidence("known_limitations", EvidenceType.CODE_DERIVED) + + register_placeholder(ability_balance_strength_metadata) + + # ── ability_balance_endurance ───────────────────────────────────────────── + + ability_balance_endurance_metadata = PlaceholderMetadata( + key="ability_balance_endurance", + description="Fähigkeiten-Balance-Score: Ausdauer (28d)", + resolver_function="get_ability_balance_endurance", + semantic_contract=( + "Liefert einen normierten Score (0-100) für die relative Trainingsbelastung " + "der Fähigkeit 'Ausdauer' über 28 Tage. Berechnung: " + "(ausdauer_load / max_load_aller_fähigkeiten) × 100. " + "Basiert auf training_types.abilities JSONB Feld (endurance_percentage)." + ), + business_meaning=( + "Balance-Metrik für Trainingsvielfalt. " + "Zeigt ob Ausdauer überproportional (>80), balanciert (40-60), oder unterrepräsentiert (<20) trainiert wird." + ), + example_output="92", + known_limitations=( + "JSONB DEPENDENCY: training_types.abilities JSONB Feld muss populated sein. " + "FORMULA: max-normalized (nicht prozentual vom Total). " + "EDGE CASE: abilities NULL → 0% Ausdauer. " + "EDGE CASE: Alle Fähigkeiten = 0 → return 0." + ), + layer_2b_reuse_possible=True, + **common_metadata_ability + ) + + ability_balance_endurance_metadata.evidence.update(common_evidence_ability) + ability_balance_endurance_metadata.set_evidence("key", EvidenceType.CODE_DERIVED) + ability_balance_endurance_metadata.set_evidence("description", EvidenceType.DRAFT_DERIVED) + ability_balance_endurance_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED) + ability_balance_endurance_metadata.set_evidence("semantic_contract", EvidenceType.CODE_DERIVED) + ability_balance_endurance_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED) + ability_balance_endurance_metadata.set_evidence("example_output", EvidenceType.CODE_DERIVED) + ability_balance_endurance_metadata.set_evidence("known_limitations", EvidenceType.CODE_DERIVED) + + register_placeholder(ability_balance_endurance_metadata) + + # ── ability_balance_mental ──────────────────────────────────────────────── + + ability_balance_mental_metadata = PlaceholderMetadata( + key="ability_balance_mental", + description="Fähigkeiten-Balance-Score: Geist & Meditation (28d)", + resolver_function="get_ability_balance_mental", + semantic_contract=( + "Liefert einen normierten Score (0-100) für die relative Trainingsbelastung " + "der Fähigkeit 'Geist & Meditation' über 28 Tage. Berechnung: " + "(mental_load / max_load_aller_fähigkeiten) × 100. " + "Basiert auf training_types.abilities JSONB Feld (mental_percentage)." + ), + business_meaning=( + "Balance-Metrik für ganzheitliches Training. " + "Zeigt ob mentale/meditative Praxis im Verhältnis zu physischem Training steht. " + "Oft niedrig (<20) bei rein physisch orientierten Trainern." + ), + example_output="15", + known_limitations=( + "JSONB DEPENDENCY: training_types.abilities JSONB Feld muss populated sein. " + "FORMULA: max-normalized (nicht prozentual vom Total). " + "EDGE CASE: abilities NULL → 0% Mental. " + "EDGE CASE: Alle Fähigkeiten = 0 → return 0." + ), + layer_2b_reuse_possible=True, + **common_metadata_ability + ) + + ability_balance_mental_metadata.evidence.update(common_evidence_ability) + ability_balance_mental_metadata.set_evidence("key", EvidenceType.CODE_DERIVED) + ability_balance_mental_metadata.set_evidence("description", EvidenceType.DRAFT_DERIVED) + ability_balance_mental_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED) + ability_balance_mental_metadata.set_evidence("semantic_contract", EvidenceType.CODE_DERIVED) + ability_balance_mental_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED) + ability_balance_mental_metadata.set_evidence("example_output", EvidenceType.CODE_DERIVED) + ability_balance_mental_metadata.set_evidence("known_limitations", EvidenceType.CODE_DERIVED) + + register_placeholder(ability_balance_mental_metadata) + + # ── ability_balance_coordination ────────────────────────────────────────── + + ability_balance_coordination_metadata = PlaceholderMetadata( + key="ability_balance_coordination", + description="Fähigkeiten-Balance-Score: Koordination (28d)", + resolver_function="get_ability_balance_coordination", + semantic_contract=( + "Liefert einen normierten Score (0-100) für die relative Trainingsbelastung " + "der Fähigkeit 'Koordination' über 28 Tage. Berechnung: " + "(koordination_load / max_load_aller_fähigkeiten) × 100. " + "Basiert auf training_types.abilities JSONB Feld (coordination_percentage)." + ), + business_meaning=( + "Balance-Metrik für motorische Vielfalt. " + "Zeigt ob Koordinations-Training (Kampfsport, Tanz, komplexe Bewegungsmuster) " + "im Trainingsplan vertreten ist. Oft niedrig (<20) bei reinem Kraft/Ausdauer-Training." + ), + example_output="28", + known_limitations=( + "JSONB DEPENDENCY: training_types.abilities JSONB Feld muss populated sein. " + "FORMULA: max-normalized (nicht prozentual vom Total). " + "EDGE CASE: abilities NULL → 0% Koordination. " + "EDGE CASE: Alle Fähigkeiten = 0 → return 0." + ), + layer_2b_reuse_possible=True, + **common_metadata_ability + ) + + ability_balance_coordination_metadata.evidence.update(common_evidence_ability) + ability_balance_coordination_metadata.set_evidence("key", EvidenceType.CODE_DERIVED) + ability_balance_coordination_metadata.set_evidence("description", EvidenceType.DRAFT_DERIVED) + ability_balance_coordination_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED) + ability_balance_coordination_metadata.set_evidence("semantic_contract", EvidenceType.CODE_DERIVED) + ability_balance_coordination_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED) + ability_balance_coordination_metadata.set_evidence("example_output", EvidenceType.CODE_DERIVED) + ability_balance_coordination_metadata.set_evidence("known_limitations", EvidenceType.CODE_DERIVED) + + register_placeholder(ability_balance_coordination_metadata) + + # ── ability_balance_mobility ────────────────────────────────────────────── + + ability_balance_mobility_metadata = PlaceholderMetadata( + key="ability_balance_mobility", + description="Fähigkeiten-Balance-Score: Mobilität (28d)", + resolver_function="get_ability_balance_mobility", + semantic_contract=( + "Liefert einen normierten Score (0-100) für die relative Trainingsbelastung " + "der Fähigkeit 'Mobilität' über 28 Tage. Berechnung: " + "(mobilität_load / max_load_aller_fähigkeiten) × 100. " + "Basiert auf training_types.abilities JSONB Feld (mobility_percentage)." + ), + business_meaning=( + "Balance-Metrik für Beweglichkeits-Training. " + "Zeigt ob Mobilität (Stretching, Yoga, Fasziendehnung) im Trainingsplan vertreten ist. " + "Wichtig für Verletzungsprävention und Regeneration. Oft vernachlässigt (<20)." + ), + example_output="22", + known_limitations=( + "JSONB DEPENDENCY: training_types.abilities JSONB Feld muss populated sein. " + "FORMULA: max-normalized (nicht prozentual vom Total). " + "EDGE CASE: abilities NULL → 0% Mobilität. " + "EDGE CASE: Alle Fähigkeiten = 0 → return 0." + ), + layer_2b_reuse_possible=True, + **common_metadata_ability + ) + + ability_balance_mobility_metadata.evidence.update(common_evidence_ability) + ability_balance_mobility_metadata.set_evidence("key", EvidenceType.CODE_DERIVED) + ability_balance_mobility_metadata.set_evidence("description", EvidenceType.DRAFT_DERIVED) + ability_balance_mobility_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED) + ability_balance_mobility_metadata.set_evidence("semantic_contract", EvidenceType.CODE_DERIVED) + ability_balance_mobility_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED) + ability_balance_mobility_metadata.set_evidence("example_output", EvidenceType.CODE_DERIVED) + ability_balance_mobility_metadata.set_evidence("known_limitations", EvidenceType.CODE_DERIVED) + + register_placeholder(ability_balance_mobility_metadata) + + # ── vo2max_trend_28d ────────────────────────────────────────────────────── + + vo2max_trend_28d_metadata = PlaceholderMetadata( + key="vo2max_trend_28d", + description="VO2 Max Trend über 28 Tage", + category="Aktivität", + resolver_module="backend/placeholder_resolver.py", + resolver_function="get_vo2max_trend_28d", + data_layer_module="backend/data_layer/activity_metrics.py", + data_layer_function="calculate_vo2max_trend", + source_tables=["vitals_baseline"], + time_window="28d", + output_type=OutputType.NUMERIC, + placeholder_type=PlaceholderType.INTERPRETED, + semantic_contract=( + "Liefert den Trend der VO2 Max Werte über 28 Tage als Delta (letzter - erster Wert). " + "Positiver Wert = Verbesserung der aeroben Fitness, " + "negativer Wert = Verschlechterung. " + "Basiert auf vitals_baseline.vo2_max (gemessen oder geschätzt)." + ), + business_meaning=( + "Fitness-Trend-Metrik für aerobe Kapazität. " + "Indikator für Trainingseffektivität bei Ausdauer-fokussiertem Training. " + "QUESTIONABLE CATEGORY: VO2 Max ist primär Vitalwert/Recovery, nicht Activity." + ), + unit="ml/kg/min (Delta)", + format_hint="Dezimalzahl mit Vorzeichen, typisch -3.0 bis +3.0", + example_output="+1.2", + minimum_data_requirements=None, + quality_filter_policy=None, + confidence_logic=( + "datenpunktbasierte Coverage-Logik. " + "Mindestens 2 VO2 Max Messungen in 28d erforderlich für Trend." + ), + missing_value_policy=MissingValuePolicy( + available=False, + value_raw=None, + missing_reason="insufficient_data", + legacy_display="nicht genug Daten" + ), + known_limitations=( + "QUESTIONABLE CATEGORY: VO2 Max primär Recovery-Cluster, nicht Activity-Cluster. " + "DEPENDENCY: vitals_baseline.vo2_max Feld (oft NULL wenn nicht gemessen). " + "EDGE CASE: Nur 1 Messung → kein Trend → missing_value. " + "EDGE CASE: Große Zeitlücken zwischen Messungen → Trend nicht aussagekräftig." + ), + layer_1_decision="Data Layer (activity_metrics.calculate_vo2max_trend) - QUESTIONABLE", + layer_2a_decision="Placeholder Resolver (formatting only)", + layer_2b_reuse_possible=True, + architecture_alignment="Phase 0c Multi-Layer Architecture conform", + issue_53_alignment="Layer separation established" + ) + + vo2max_trend_28d_metadata.set_evidence("key", EvidenceType.CODE_DERIVED) + vo2max_trend_28d_metadata.set_evidence("category", EvidenceType.TO_VERIFY) + vo2max_trend_28d_metadata.set_evidence("description", EvidenceType.DRAFT_DERIVED) + vo2max_trend_28d_metadata.set_evidence("resolver_module", EvidenceType.CODE_DERIVED) + vo2max_trend_28d_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED) + vo2max_trend_28d_metadata.set_evidence("data_layer_module", EvidenceType.CODE_DERIVED) + vo2max_trend_28d_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED) + vo2max_trend_28d_metadata.set_evidence("source_tables", EvidenceType.CODE_DERIVED) + vo2max_trend_28d_metadata.set_evidence("time_window", EvidenceType.CODE_DERIVED) + vo2max_trend_28d_metadata.set_evidence("output_type", EvidenceType.CODE_DERIVED) + vo2max_trend_28d_metadata.set_evidence("placeholder_type", EvidenceType.MIXED) + vo2max_trend_28d_metadata.set_evidence("semantic_contract", EvidenceType.MIXED) + vo2max_trend_28d_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED) + vo2max_trend_28d_metadata.set_evidence("unit", EvidenceType.CODE_DERIVED) + vo2max_trend_28d_metadata.set_evidence("format_hint", EvidenceType.DRAFT_DERIVED) + vo2max_trend_28d_metadata.set_evidence("example_output", EvidenceType.CODE_DERIVED) + vo2max_trend_28d_metadata.set_evidence("minimum_data_requirements", EvidenceType.UNRESOLVED) + vo2max_trend_28d_metadata.set_evidence("quality_filter_policy", EvidenceType.UNRESOLVED) + vo2max_trend_28d_metadata.set_evidence("confidence_logic", EvidenceType.CODE_DERIVED) + vo2max_trend_28d_metadata.set_evidence("missing_value_policy", EvidenceType.CODE_DERIVED) + vo2max_trend_28d_metadata.set_evidence("known_limitations", EvidenceType.MIXED) + vo2max_trend_28d_metadata.set_evidence("layer_1_decision", EvidenceType.CODE_DERIVED) + vo2max_trend_28d_metadata.set_evidence("layer_2a_decision", EvidenceType.CODE_DERIVED) + vo2max_trend_28d_metadata.set_evidence("layer_2b_reuse_possible", EvidenceType.TO_VERIFY) + vo2max_trend_28d_metadata.set_evidence("architecture_alignment", EvidenceType.CODE_DERIVED) + vo2max_trend_28d_metadata.set_evidence("issue_53_alignment", EvidenceType.CODE_DERIVED) + + register_placeholder(vo2max_trend_28d_metadata) + + # ── activity_score ──────────────────────────────────────────────────────── + + activity_score_metadata = PlaceholderMetadata( + key="activity_score", + description="Gesamtaktivitäts-Score (gewichtet)", + category="Aktivität", + resolver_module="backend/placeholder_resolver.py", + resolver_function="get_activity_score", + data_layer_module="backend/data_layer/scores.py", + data_layer_function="calculate_activity_score", + source_tables=["activity_log", "training_types", "rest_days", "vitals_baseline", "user_focus_area_weights"], + time_window="composite (7d, 14d, 28d mixed)", + output_type=OutputType.NUMERIC, + placeholder_type=PlaceholderType.SCORE, + semantic_contract=( + "Liefert einen gewichteten Gesamtscore (0-100) für Trainingsaktivität. " + "Berechnung: Gewichteter Durchschnitt aus 5 Komponenten: " + "1. Volumen (training_minutes_week vs. Ziel), " + "2. Frequenz (training_frequency_7d vs. Ziel), " + "3. Qualität (quality_sessions_pct), " + "4. Balance (Ability Balance Scores), " + "5. Recovery (rest_day_compliance). " + "Gewichtung dynamisch aus user_focus_area_weights." + ), + business_meaning=( + "Meta-Score für Gesamtaktivitäts-Qualität. " + "Kombiniert Volumen, Konsistenz, Qualität, Vielfalt und Regeneration. " + "Ziel-abhängig: Gewichtung variiert je nach User-Fokus (Kraft vs. Ausdauer vs. Balance)." + ), + unit="Score (0-100)", + format_hint="Ganzzahl", + example_output="78", + minimum_data_requirements=None, + quality_filter_policy=None, + confidence_logic=( + "Composite Confidence basierend auf Komponenten-Confidence. " + "Mindestens 3/5 Komponenten müssen verfügbar sein für Score-Berechnung." + ), + missing_value_policy=MissingValuePolicy( + available=False, + value_raw=None, + missing_reason="insufficient_data", + legacy_display="nicht genug Daten" + ), + known_limitations=( + "COMPOSITE SCORE: Komplexe Abhängigkeiten zu allen Activity-Metriken. " + "FOCUS-DEPENDENT: Gewichtung variiert je nach user_focus_area_weights (dynamisch). " + "EDGE CASE: Wenn user_focus_area_weights fehlt → default weighting (alle gleich). " + "ZEITFENSTER MIXED: Komponenten nutzen unterschiedliche Zeitfenster (7d, 14d, 28d). " + "QUESTIONABLE: Vermischt Metriken mit unterschiedlicher Verlässlichkeit " + "(z.B. quality_sessions_pct hat TO_VERIFY Issues)." + ), + layer_1_decision="Data Layer (scores.calculate_activity_score)", + layer_2a_decision="Placeholder Resolver (formatting only)", + layer_2b_reuse_possible=False, + architecture_alignment="Phase 0c Multi-Layer Architecture conform", + issue_53_alignment="Layer separation established" + ) + + activity_score_metadata.set_evidence("key", EvidenceType.CODE_DERIVED) + activity_score_metadata.set_evidence("category", EvidenceType.CODE_DERIVED) + activity_score_metadata.set_evidence("description", EvidenceType.DRAFT_DERIVED) + activity_score_metadata.set_evidence("resolver_module", EvidenceType.CODE_DERIVED) + activity_score_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED) + activity_score_metadata.set_evidence("data_layer_module", EvidenceType.CODE_DERIVED) + activity_score_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED) + activity_score_metadata.set_evidence("source_tables", EvidenceType.CODE_DERIVED) + activity_score_metadata.set_evidence("time_window", EvidenceType.CODE_DERIVED) + activity_score_metadata.set_evidence("output_type", EvidenceType.CODE_DERIVED) + activity_score_metadata.set_evidence("placeholder_type", EvidenceType.MIXED) + activity_score_metadata.set_evidence("semantic_contract", EvidenceType.CODE_DERIVED) + activity_score_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED) + activity_score_metadata.set_evidence("unit", EvidenceType.CODE_DERIVED) + activity_score_metadata.set_evidence("format_hint", EvidenceType.CODE_DERIVED) + activity_score_metadata.set_evidence("example_output", EvidenceType.CODE_DERIVED) + activity_score_metadata.set_evidence("minimum_data_requirements", EvidenceType.UNRESOLVED) + activity_score_metadata.set_evidence("quality_filter_policy", EvidenceType.UNRESOLVED) + activity_score_metadata.set_evidence("confidence_logic", EvidenceType.MIXED) + activity_score_metadata.set_evidence("missing_value_policy", EvidenceType.CODE_DERIVED) + activity_score_metadata.set_evidence("known_limitations", EvidenceType.CODE_DERIVED) + activity_score_metadata.set_evidence("layer_1_decision", EvidenceType.CODE_DERIVED) + activity_score_metadata.set_evidence("layer_2a_decision", EvidenceType.CODE_DERIVED) + activity_score_metadata.set_evidence("layer_2b_reuse_possible", EvidenceType.CODE_DERIVED) + activity_score_metadata.set_evidence("architecture_alignment", EvidenceType.CODE_DERIVED) + activity_score_metadata.set_evidence("issue_53_alignment", EvidenceType.CODE_DERIVED) + + register_placeholder(activity_score_metadata) + + +# ============================================================================= +# Auto-Registration +# ============================================================================= + +# Register all groups when module is imported +register_activity_group_1() +register_activity_group_2() +register_activity_group_3()