feat: Enhance activity metrics documentation and registry updates
All checks were successful
Deploy Development / deploy (push) Successful in 48s
Build Test / pytest-backend (push) Successful in 4s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 16s

- 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.
This commit is contained in:
Lars 2026-04-17 20:28:58 +02:00
parent 680ecd1c06
commit c3be745efa
7 changed files with 130 additions and 47 deletions

View File

@ -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 12) + **Phasenplan AF** 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) |

View File

@ -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 **Layer1Quelle** (`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 **Layer2a**-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 Layer1-/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)

View File

@ -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 AF** 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).

View File

@ -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)

View File

@ -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)

View File

@ -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')",

View File

@ -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,