From c3be745efac1663e3ecf3126d16dbea99600f632 Mon Sep 17 00:00:00 2001 From: Lars Date: Fri, 17 Apr 2026 20:28:58 +0200 Subject: [PATCH] feat: Enhance activity metrics documentation and registry updates - Added details for Issue #53 regarding the audit of activity placeholders between Layer 1 and Layer 2a in `CLAUDE.md` and `README.md`. - Updated the `ACTIVITY_SESSION_METRICS_EAV_AGENT_GUIDE.md` to reflect the new registry checks and dynamic session metrics handling. - Revised the `placeholder_resolver.py` and `activity_metrics.py` to clarify the registration of activity metrics and session insights, ensuring consistency in the handling of dynamic keys and metrics. - Improved descriptions and semantic contracts in `activity_session_insights.py` to better outline the structure and limitations of session data. --- .claude/docs/README.md | 1 + .../ACTIVITY_LAYER2A_PLACEHOLDER_AUDIT.md | 70 +++++++++++++++++ ...CTIVITY_SESSION_METRICS_EAV_AGENT_GUIDE.md | 2 + CLAUDE.md | 2 + .../activity_metrics.py | 77 +++++++++---------- .../activity_session_insights.py | 23 ++++-- backend/placeholder_resolver.py | 2 +- 7 files changed, 130 insertions(+), 47 deletions(-) create mode 100644 .claude/docs/technical/ACTIVITY_LAYER2A_PLACEHOLDER_AUDIT.md diff --git a/.claude/docs/README.md b/.claude/docs/README.md index 0cff71e..8c409d9 100644 --- a/.claude/docs/README.md +++ b/.claude/docs/README.md @@ -117,6 +117,7 @@ _Dieser Ordner `.claude/docs/` ist per `.gitignore`-Ausnahme **versioniert** (Sp | `ACTIVITY_SESSION_METRICS_EAV_AGENT_GUIDE.md` | Session-Metriken EAV, Attributprofile, Layer-1, Prod-Migration | | `ACTIVITY_COMPOSITE_METRICS_IMPLEMENTATION_CONCEPT.md` | Composite-Metriken in EAV (JSONB), Archetypen, CSV-Slots, Layer-1-Expand, Migration/Test-Checkliste | | `ACTIVITY_PRODUCTION_ARCHITECTURE_AND_PHASES.md` | **Zielarchitektur** Aktivität (Spine/EAV/Composites/Import/Layer 1–2) + **Phasenplan A–F** Produktionsreife | +| `ACTIVITY_LAYER2A_PLACEHOLDER_AUDIT.md` | Issue #53: Aktivitäts-Platzhalter Layer 1 ↔ 2a (Audit Schritt 1) | | `ACTIVITY_SCALAR_KANON_TABLE.md` | **Skalar-Kanon** Aktivität (eine Semantik → eine Quelle); Phase A | | *(Code)* `backend/data_layer/activity_data_canon.py` | **Kanon** activity CSV-Modul vs. EAV-primär; Legacy-Lesefallback | | `V9D_PHASE2_VITALS_SLEEP.md` | v9d Vitalwerte/Schlaf (Release-Bezug) | diff --git a/.claude/docs/technical/ACTIVITY_LAYER2A_PLACEHOLDER_AUDIT.md b/.claude/docs/technical/ACTIVITY_LAYER2A_PLACEHOLDER_AUDIT.md new file mode 100644 index 0000000..fd3caed --- /dev/null +++ b/.claude/docs/technical/ACTIVITY_LAYER2A_PLACEHOLDER_AUDIT.md @@ -0,0 +1,70 @@ +# Aktivität: Layer-2a-Platzhalter — Audit Schritt 1 (Issue #53) + +**Stand:** 2026-04-16 +**Bezug:** [Issue #53 — Multi-Layer Architecture](../../../docs/issues/issue-53-phase-0c-multi-layer-architecture.md): Layer 1 = strukturierte Daten, Layer 2a = KI-Formatierung (keine parallele Domänen-Logik im Resolver). + +**Ziel dieses Dokuments:** Jeder Aktivitäts-Platzhalter hat genau eine **Layer‑1‑Quelle** (`data_layer/activity_metrics.py`); `placeholder_resolver.py` formatiert oder serialisiert nur noch. + +--- + +## 1. Ergebnisübersicht + +| Kategorie | Anzahl | Resolver-SQL für Aktivität? | +|-----------|--------|------------------------------| +| Gebündelt in `PLACEHOLDER_MAP` (Training/Aktivität) | 20 | **Nein** | +| Abweichungen / offene Punkte | 0 | — | + +**Hinweis:** `{{rest_days_count}}` steht in der Karte unter „Schlaf & Erholung“ und nutzt `recovery_metrics.get_rest_days_data` — nicht in dieser Tabelle. + +--- + +## 2. Platzhalter → Layer 1 → Layer 2a + +| Key | Layer 1 (`activity_metrics`) | Layer 2a (`placeholder_resolver`) | Bemerkung | +|-----|------------------------------|-------------------------------------|-----------| +| `activity_summary` | `get_activity_summary_data` | `get_activity_summary` | String-Zusammenfassung | +| `activity_detail` | `get_activity_detail_data` (+ `enrich_sessions_with_metrics`) | `get_activity_detail` | Dynamische `session_metrics[]` pro Zeile (Profil/EAV) | +| `trainingstyp_verteilung` | `get_training_type_distribution_data` | `get_trainingstyp_verteilung` | Ausgabe: Top-3-Text (kein JSON); Registry 2026-04 an Ist angeglichen | +| `training_minutes_week` | `calculate_training_minutes_week` | `_safe_int` | | +| `training_frequency_7d` | `calculate_training_frequency_7d` | `_safe_int` | | +| `quality_sessions_pct` | `calculate_quality_sessions_pct` | `_safe_int` | | +| `proxy_internal_load_7d` | `calculate_proxy_internal_load_7d` | `_safe_int` | | +| `monotony_score` | `calculate_monotony_score` | `_safe_float` | | +| `strain_score` | `calculate_strain_score` | `_safe_int` | | +| `rest_day_compliance` | `calculate_rest_day_compliance` | `_safe_int` | | +| `ability_balance_strength` | `calculate_ability_balance_strength` | `_safe_int` | abilities in `activity_log` | +| `ability_balance_endurance` | `calculate_ability_balance_endurance` | `_safe_int` | | +| `ability_balance_mental` | `calculate_ability_balance_mental` | `_safe_int` | | +| `ability_balance_coordination` | `calculate_ability_balance_coordination` | `_safe_int` | | +| `ability_balance_mobility` | `calculate_ability_balance_mobility` | `_safe_int` | | +| `vo2max_trend_28d` | `calculate_vo2max_trend_28d` | `_safe_float` | | +| `activity_score` | `calculate_activity_score` | `_safe_int` | | +| `training_frequency_by_type_md` | `get_training_frequency_by_type_data` | `get_training_frequency_by_type_md` | Markdown-Tabelle | +| `training_inter_session_gap_md` | `get_training_inter_session_gap_data` | `get_training_inter_session_gap_md` | Markdown-Text | +| `training_sessions_recent_json` | `get_training_sessions_recent_weeks_data` (+ `enrich_sessions_with_metrics`) | `_safe_json('training_sessions_recent_json')` | JSON inkl. `session_metrics[]` pro Session | + +--- + +## 3. Schichten-Disziplin (Checkliste) + +- [x] Kein `SELECT` auf `activity_log` / `activity_session_metrics` in den **Layer‑2a**-Funktionen oben — nur Aufrufe in Layer 1 bzw. `_safe_*`-Wrapper. +- [x] `get_activity_detail` / `get_training_sessions_recent_json` liefern EAV nur über **bereits gemergte** `session_metrics` (Merge-Kanon: `activity_log` vor EAV). +- [x] Registry-Metadaten: `data_layer_module` / `data_layer_function` pro Key in `placeholder_registrations/activity_metrics.py` und `activity_session_insights.py`. +- [x] Korrektur Registry: `activity_summary.resolver_function` = `get_activity_summary` (war veraltet: `_format_activity_summary`). + +--- + +## 4. Nächste Schritte (Roadmap) + +2. ~~**Registry-Texte:** `semantic_contract` / `known_limitations` für dynamische `session_metrics` (tcp/ttp) und Merge-Kanon — **erledigt** (`activity_detail`, `training_sessions_recent_json`); dazu **`trainingstyp_verteilung`**-Metadaten von veraltetem „JSON/Resolver-SQL“ auf Ist (**Layer 1 + Top-3-Text**) korrigiert.~~ +3. **History / Layer 2b:** EAV-Zeitreihen nicht über Platzhalter, sondern dedizierte Layer‑1-/Chart-Pfade. +4. **Optional:** Gitea-Issue „Activity Layer 2a“ bei Änderungen an `activity_metrics` pflegen. + +--- + +## 5. Referenzen + +- `backend/placeholder_resolver.py` — `PLACEHOLDER_MAP` (Training/Aktivität) +- `backend/placeholder_registrations/activity_metrics.py` +- `backend/placeholder_registrations/activity_session_insights.py` +- `ACTIVITY_PRODUCTION_ARCHITECTURE_AND_PHASES.md` §2.1a (Navigation Read vs. Berechnen) diff --git a/.claude/docs/technical/ACTIVITY_SESSION_METRICS_EAV_AGENT_GUIDE.md b/.claude/docs/technical/ACTIVITY_SESSION_METRICS_EAV_AGENT_GUIDE.md index c0a8df3..4aacfbd 100644 --- a/.claude/docs/technical/ACTIVITY_SESSION_METRICS_EAV_AGENT_GUIDE.md +++ b/.claude/docs/technical/ACTIVITY_SESSION_METRICS_EAV_AGENT_GUIDE.md @@ -89,6 +89,8 @@ Router: `backend/routers/admin_training_parameters.py`, `backend/routers/admin_a ## 5. Agent-Checkliste (nächste Iterationen) +**Layer 2a (Platzhalter Aktivität):** Abgleich Registry ↔ Resolver ↔ Layer 1 — [`ACTIVITY_LAYER2A_PLACEHOLDER_AUDIT.md`](./ACTIVITY_LAYER2A_PLACEHOLDER_AUDIT.md) (Issue #53). **Schritt 2:** `semantic_contract` / `known_limitations` für dynamische `session_metrics` und Korrektur `trainingstyp_verteilung` in der Registry. + Siehe **Phasen A–F** in [`ACTIVITY_PRODUCTION_ARCHITECTURE_AND_PHASES.md`](./ACTIVITY_PRODUCTION_ARCHITECTURE_AND_PHASES.md). Kurz: - [x] **Phase A:** Kanon-Tabelle (eine Quelle pro Semantik) — [`ACTIVITY_SCALAR_KANON_TABLE.md`](./ACTIVITY_SCALAR_KANON_TABLE.md). diff --git a/CLAUDE.md b/CLAUDE.md index b863774..be58c60 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -128,6 +128,8 @@ frontend/src/ - **Phase A:** Skalar-Kanon schriftlich fixiert — `.claude/docs/technical/ACTIVITY_SCALAR_KANON_TABLE.md`; `ACTIVITY_PRODUCTION_ARCHITECTURE_AND_PHASES.md` v1.1; Agent-Guide Checkliste Phase A erledigt. - **Phase B:** `GET /api/activity` (Liste) reichert jede Zeile mit `session_metrics` über `enrich_sessions_with_metrics` an (gleiche Merge-Logik wie Detail); Consumer-Audit-Tabelle in Produktions-Architektur-Dok §4 Phase B. - **Phase B (Export):** `routers/exportdata.py` — JSON-Export `activity` mit `session_metrics`; CSV-Gesamtexport Training-Details mit EAV-Zusammenfassung; ZIP `data/activity.csv` mit Zusatzspalte `session_metrics_json` (Standard-Import unverändert). +- **Issue #53 / Layer 2a:** `ACTIVITY_LAYER2A_PLACEHOLDER_AUDIT.md` — alle 20 Aktivitäts-Platzhalter gegen Layer 1 geprüft; Registry-Fix `activity_summary.resolver_function` → `get_activity_summary`. +- **Layer 2a Schritt 2:** Registry-Texte `activity_detail`, `training_sessions_recent_json` (dynamische session_metrics, Merge-Kanon); `trainingstyp_verteilung` Metadaten an Phase-0c-Code angeglichen. ### Updates (11.04.2026 - Ernährung: TDEE, Bilanz, Kalorien-Score) diff --git a/backend/placeholder_registrations/activity_metrics.py b/backend/placeholder_registrations/activity_metrics.py index 5a01a20..544249e 100644 --- a/backend/placeholder_registrations/activity_metrics.py +++ b/backend/placeholder_registrations/activity_metrics.py @@ -1,7 +1,7 @@ """ Activity Metrics Placeholder Registrations -Registers 17 Aktivitäts-Platzhalter hier; 3 Session-/Erholungs-Keys in activity_session_insights.py (20 gesamt). +Registers 17 Aktivitäts-Platzhalter hier; 3 weitere Keys in activity_session_insights.py (**20 gesamt** in PLACEHOLDER_MAP). Evidence-based metadata with clear tagging of source. @@ -43,7 +43,7 @@ def register_activity_group_1(): category="Aktivität", description="Zusammenfassung der letzten 14 Tage Aktivität", resolver_module="backend/placeholder_resolver.py", - resolver_function="_format_activity_summary", + resolver_function="get_activity_summary", data_layer_module=None, data_layer_function=None, source_tables=["activity_log", "training_types"], @@ -127,17 +127,23 @@ def register_activity_group_1(): activity_detail_metadata = PlaceholderMetadata( key="activity_detail", category="Aktivität", - description="Detaillierte Liste der letzten 14 Tage Aktivität (Kopfzeile + EAV-Metriken)", + description=( + "Letzte 14 Tage: pro Session Kopfzeile (activity_log) plus gemergte Profil-Metriken " + "(dynamische Keys je training_category / training_type_id)" + ), resolver_module="backend/placeholder_resolver.py", resolver_function="get_activity_detail", data_layer_module="backend/data_layer/activity_metrics.py", data_layer_function="get_activity_detail_data", source_tables=["activity_log", "activity_session_metrics", "training_parameters"], semantic_contract=( - "Liefert bis zu 50 Einheiten (neueste zuerst) der letzten 14 Tage über " - "get_activity_detail_data: activity_log-Spalten plus " - "enrich_sessions_with_metrics (activity_session_metrics / Profil-EAV). " - "Formatter hängt nicht-leere EAV-Werte als „| EAV: key=value; …“ an." + "Layer 1: get_activity_detail_data lädt Sessions, enrich_sessions_with_metrics fügt " + "session_metrics hinzu — effektive Liste aus merge_column_backed_and_eav_metrics: nur " + "Parameter aus dem Attributschema (tcp/ttp), sortiert nach key. " + "Leseregel Kanon: activity_log-Spalte (source_field, Registry-Feld, Legacy-Spalte für " + "EAV-primäre Keys) schlägt EAV, wenn beide Werte liefern. " + "Layer 2a: Zeilen mit „| EAV: key=value; …“ nur für nicht-leere session_metrics; " + "die Menge der Keys ist admin-/profilabhängig, kein festes Prompt-Schema." ), business_meaning=( "Detaillierte Trainingshistorie für KI-Prompts, die Muster, Progressionen " @@ -167,8 +173,10 @@ def register_activity_group_1(): ), known_limitations=( "Keine Profil-Qualitätsfilterung in dieser Liste. Max. 20 Zeilen im Prompt-Output " - "(Hard-Limit Resolver). Doppelte Spalten (z.B. duration_min in Kopf und EAV) können " - "in EAV wiederholt erscheinen — KI kann dominante Spalte nutzen." + "(Hard-Limit Resolver). session_metrics kann leer sein (kein Typ, kein Profil, keine EAV-Zeilen). " + "Keys und Anzahl Metriken variieren je Instanz/Admin — nicht von festen Platzhaltern in anderen " + "Prompts ausgehen. Nur im effektiven Merge erscheinende Parameter; keine verwaisten EAV-Keys " + "außerhalb des Schemas." ), layer_1_decision="activity_metrics.get_activity_detail_data (+ enrich_sessions_with_metrics)", layer_2a_decision="get_activity_detail (Formatierung)", @@ -211,56 +219,47 @@ def register_activity_group_1(): trainingstyp_verteilung_metadata = PlaceholderMetadata( key="trainingstyp_verteilung", category="Aktivität", - description="Trainingstypen-Verteilung der letzten 14 Tage als JSON", + description="Verteilung nach training_category (14 Tage): Top 3 als kompakte Prozent-Textzeile", 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"], + resolver_function="get_trainingstyp_verteilung", + data_layer_module="backend/data_layer/activity_metrics.py", + data_layer_function="get_training_type_distribution_data", + source_tables=["activity_log"], 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." + "Layer 1: get_training_type_distribution_data — Anteil je training_category am " + "Gesamt-Session-Count im Fenster (auch unkategorisierte zählen im Nenner). " + "Layer 2a: Top 3 Kategorien als „Name: p%“ kommagetrennt; bei fehlenden Daten Kurz-Hinweis." ), 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", + unit="text", time_window="14d", - output_type=OutputType.JSON, + output_type=OutputType.TEXT_SUMMARY, 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}}' - ), + format_hint="Eine Zeile: bis zu drei „Kategorie: Prozent%“, durch Komma getrennt", + example_output="cardio: 45%, strength: 30%, mobility: 15%", minimum_data_requirements=None, quality_filter_policy=None, - confidence_logic="Keine Confidence-Berechnung. Aggregation basiert auf verfügbaren Daten.", + confidence_logic="Wie get_training_type_distribution_data (calculate_confidence über categorized_count)", missing_value_policy=MissingValuePolicy( available=False, value_raw=None, missing_reason="no_data", - legacy_display="{}" + legacy_display="Keine kategorisierten Trainings" ), 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)." + "Nur Sessions mit gesetztem training_category fließen in die Verteilungsliste; " + "Prozente beziehen sich auf alle Sessions im Fenster (Nenner = total_sessions). " + "Keine Qualitätsfilterung der Einheiten. Kein drill-down nach training_type_id in diesem Platzhalter." ), - layer_1_decision="NONE - Old resolver pattern (direct SQL aggregation in resolver)", - layer_2a_decision="Placeholder Resolver (aggregation + JSON formatting)", + layer_1_decision="activity_metrics.get_training_type_distribution_data", + layer_2a_decision="get_trainingstyp_verteilung (Top 3 als Text)", 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" + architecture_alignment="Phase 0c — Layer 1 + Formatierung", + issue_53_alignment="Layer 1" ) trainingstyp_verteilung_metadata.set_evidence("key", EvidenceType.CODE_DERIVED) diff --git a/backend/placeholder_registrations/activity_session_insights.py b/backend/placeholder_registrations/activity_session_insights.py index faef32a..38fcd63 100644 --- a/backend/placeholder_registrations/activity_session_insights.py +++ b/backend/placeholder_registrations/activity_session_insights.py @@ -130,8 +130,8 @@ def register_activity_session_insights(): key="training_sessions_recent_json", category="Aktivität", description=( - "JSON: letzte ISO-Kalenderwochen mit Einheiten (Datum, Art, Dauer, kcal, HF Ø/max, RPE, Kategorie, " - "session_id, session_metrics[] aus EAV)" + "JSON: ISO-Wochen mit Sessions (activity_log-Kopf) plus session_metrics[] — gemergte Profil-Metriken " + "(dynamische Keys)" ), resolver_module="backend/placeholder_resolver.py", resolver_function="_safe_json", @@ -139,9 +139,15 @@ def register_activity_session_insights(): data_layer_function="get_training_sessions_recent_weeks_data", source_tables=["activity_log", "training_types", "activity_session_metrics", "training_parameters"], semantic_contract=( - "Struktur weeks[].week_iso, sessions[] mit Feldern für KI-Auswertung; " - "session_metrics[] = Layer-1-EAV-Werte (key, data_type, unit, value) wenn konfiguriert/gespeichert. " - "Default 4 ISO-Wochen zurück." + "Root: weeks[] mit week_iso; sessions[] pro Einheit u. a. id, date, activity_type, " + "duration_min, kcal_active, hr_avg, hr_max, rpe, training_category, training_type_name, " + "session_metrics[]. " + "session_metrics: effektive Liste nach merge_column_backed_and_eav_metrics — Einträge mit " + "training_parameter_id, key, data_type, unit, value; nur Parameter aus Attributschema " + "(training_category_parameter + training_type_parameter Overrides), keys sortiert. " + "Kanon Lesen: activity_log-Spalte vor EAV bei Konflikt. " + "meta: weeks_requested, days_loaded, session_count, confidence. " + "Default ca. 4 ISO-Wochen (28 Tage Rohdatenfenster)." ), business_meaning="Rohkontext für wochenweise Auswertung (Erholung, Intensität) in der KI", unit="JSON string", @@ -160,8 +166,11 @@ def register_activity_session_insights(): legacy_display="{}", ), known_limitations=( - "Token-Länge bei vielen Sessions beachten. training_type_name nur bei gesetztem training_type_id. " - "session_metrics nur befüllt, wenn Admin-Profile zugeordnet und Werte in EAV gespeichert sind." + "Token-Länge bei vielen Sessions. training_type_name nur bei gesetztem training_type_id. " + "session_metrics oft [] (kein Typ, kein Profil, keine gespeicherten Werte). " + "Anzahl und Namen der Metrik-Keys sind instanz-/adminabhängig — JSON nicht als festes Schema " + "für Downstream-Parsing harter Logik verwenden. " + "Composite-Parameter (JSON in EAV) noch nicht im MVP expandiert; ggf. Roh-value_text in späterer Phase." ), layer_1_decision="activity_metrics.get_training_sessions_recent_weeks_data", layer_2a_decision="_safe_json('training_sessions_recent_json')", diff --git a/backend/placeholder_resolver.py b/backend/placeholder_resolver.py index 7c97dab..7e35202 100644 --- a/backend/placeholder_resolver.py +++ b/backend/placeholder_resolver.py @@ -1524,7 +1524,7 @@ PLACEHOLDER_MAP: Dict[str, Callable[[str], str]] = { '{{intake_volatility}}': lambda pid: _safe_str('intake_volatility', pid), '{{nutrition_score}}': lambda pid: _safe_int('nutrition_score', pid), - # Training / Aktivität (17 Registry-Keys — gebündelt; activity_score hier, nicht unter Meta Scores) + # Training / Aktivität (20 Keys: 17 activity_metrics + 3 activity_session_insights; activity_score hier, nicht unter Meta Scores) '{{activity_summary}}': get_activity_summary, '{{activity_detail}}': get_activity_detail, '{{trainingstyp_verteilung}}': get_trainingstyp_verteilung,