- .gitignore: .claude/docs, rules, commands tracken; settings.local weiter ignorieren - DOCUMENTATION.md: verbindliche Ablage functional/technical/working/issues - .claude/README.md: Agent-Einstieg; GITEA_ISSUES_INDEX aus MCP (Stand 2026-04-08) - Arbeitspapiere von docs/ nach .claude/docs/working/ verschoben - docs/MEMBERSHIP_SYSTEM.md als Stub; kanonisch technical/MEMBERSHIP_SYSTEM.md - CLAUDE.md Pflichtlektüre und Links angepasst; docs/README.md vereinfacht Made-with: Cursor
18 KiB
Placeholder Development Guide
Version: 1.0 Erstellt: 28. März 2026 Zielgruppe: Entwickler, Claude Code
Überblick
Dieses Dokument beschreibt, wie neue KI-Platzhalter hinzugefügt, getestet und dokumentiert werden.
Wichtig für Phase 0c: Nach dem Refactoring zu Multi-Layer Architecture nutzen alle Platzhalter das Data Layer. Dieser Guide beschreibt beide Architekturen.
Phase 0b Architektur (Aktuell - bis Phase 0c)
Anatomie eines Platzhalters
# backend/placeholder_resolver.py
def resolve_weight_28d_trend_slope(profile_id: str) -> str:
"""
Returns kg/week slope for 28-day weight trend.
This function:
1. Retrieves data from database
2. Performs calculation
3. Formats result for KI consumption
Args:
profile_id: User profile ID
Returns:
Formatted string (e.g., "0.23 kg/Woche")
or "Nicht genug Daten" if insufficient data
"""
with get_db() as conn:
cur = get_cursor(conn)
# 1. DATA RETRIEVAL
cur.execute("""
SELECT date, weight
FROM weight_log
WHERE profile_id = %s
AND date >= NOW() - INTERVAL '28 days'
ORDER BY date
""", (profile_id,))
rows = cur.fetchall()
# 2. VALIDATION
if len(rows) < 18: # Confidence threshold
return "Nicht genug Daten"
# 3. CALCULATION
x = [(row[0] - rows[0][0]).days for row in rows]
y = [row[1] for row in rows]
# Linear regression
n = len(x)
sum_x = sum(x)
sum_y = sum(y)
sum_xy = sum(xi * yi for xi, yi in zip(x, y))
sum_x2 = sum(xi ** 2 for xi in x)
slope = (n * sum_xy - sum_x * sum_y) / (n * sum_x2 - sum_x ** 2)
slope_per_week = slope * 7
# 4. FORMATTING
return f"{slope_per_week:.2f} kg/Woche"
Schritte zum Hinzufügen eines neuen Platzhalters (Phase 0b)
1. Funktion implementieren
Datei: backend/placeholder_resolver.py
Namenskonvention:
resolve_<placeholder_name>(profile_id: str) -> str- Snake_case
- Immer
profile_idals Parameter - Immer
strals Return-Type
Template:
def resolve_my_new_metric(profile_id: str) -> str:
"""
[Beschreibung was der Platzhalter zurückgibt]
Args:
profile_id: User profile ID
Returns:
[Beschreibung des Return-Formats]
"""
with get_db() as conn:
cur = get_cursor(conn)
# 1. DATA RETRIEVAL
cur.execute("""
SELECT ...
FROM ...
WHERE profile_id = %s
""", (profile_id,))
# 2. VALIDATION
if <insufficient_data_condition>:
return "Nicht genug Daten"
# 3. CALCULATION
result = ...
# 4. FORMATTING
return f"{result}"
2. In Mapping registrieren
Datei: backend/placeholder_resolver.py
Finde PLACEHOLDER_FUNCTIONS Dictionary:
PLACEHOLDER_FUNCTIONS = {
# ... existing placeholders ...
# Add your new placeholder:
"my_new_metric": resolve_my_new_metric,
}
Naming:
- Key = Platzhalter-Name (snake_case)
- Value = Funktions-Referenz (ohne Klammern!)
3. In Katalog dokumentieren
Datei: backend/placeholder_resolver.py
Finde get_placeholder_catalog() Funktion:
def get_placeholder_catalog(profile_id: str) -> Dict[str, List[Dict[str, str]]]:
placeholders = {
'Körper': [
# ... existing ...
('my_new_metric', 'Beschreibung des Platzhalters'),
],
# ...
}
Kategorien:
ProfilKörperErnährungTrainingSchlaf & ErholungVitalwerteScores (Phase 0b)Focus AreasZeitraum
4. Testen
Manueller Test:
# In Python REPL oder test script:
from placeholder_resolver import resolve_my_new_metric
result = resolve_my_new_metric("test_profile_id")
print(result) # Should return formatted string
Integration Test:
# Test in actual prompt
from placeholder_resolver import resolve_placeholders
template = "Dein {{my_new_metric}} ist ..."
result = resolve_placeholders(template, "test_profile_id")
print(result) # Should have placeholder replaced
Phase 0c Architektur (Nach Refactoring)
Anatomie eines Platzhalters (3-Layer)
# Layer 1: DATA LAYER
# backend/data_layer/body_metrics.py
def get_weight_trend_data(profile_id: str, days: int = 90) -> dict:
"""
Returns weight trend data with slopes and projections.
This is pure data retrieval and calculation.
NO FORMATTING. NO STRINGS.
Args:
profile_id: User profile ID
days: Analysis window
Returns:
{
"raw_values": [(date, weight), ...],
"rolling_median_7d": [(date, value), ...],
"slope_7d": float,
"slope_28d": float,
"slope_90d": float,
"confidence": str,
...
}
"""
with get_db() as conn:
cur = get_cursor(conn)
# DATA RETRIEVAL
cur.execute("""...""", (profile_id, days))
rows = cur.fetchall()
# VALIDATION + CONFIDENCE
from data_layer.utils import calculate_confidence
confidence = calculate_confidence(len(rows), days, "trend")
if confidence == 'insufficient':
return {
"confidence": "insufficient",
"slope_28d": 0.0,
# ... minimal data
}
# CALCULATION
# ... (same logic as before)
# RETURN STRUCTURED DATA (not formatted!)
return {
"raw_values": rows,
"slope_7d": slope_7d,
"slope_28d": slope_28d,
"confidence": confidence,
# ... all data as dict/list/float
}
# Layer 2a: KI LAYER
# backend/placeholder_resolver.py
from data_layer.body_metrics import get_weight_trend_data
def resolve_weight_28d_trend_slope(profile_id: str) -> str:
"""
Formats weight trend slope for KI consumption.
This function is now THIN - just calls data layer and formats.
"""
data = get_weight_trend_data(profile_id, days=28)
if data['confidence'] == 'insufficient':
return "Nicht genug Daten"
return f"{data['slope_28d']:.2f} kg/Woche"
Schritte zum Hinzufügen eines neuen Platzhalters (Phase 0c)
1. Data Layer Funktion implementieren
Datei: Passendes Modul in backend/data_layer/
- Body metrics →
body_metrics.py - Nutrition →
nutrition_metrics.py - Activity →
activity_metrics.py - Recovery →
recovery_metrics.py - Health →
health_metrics.py - Goals →
goals.py - Correlations →
correlations.py
Template:
# backend/data_layer/<module>.py
def get_<metric>_data(
profile_id: str,
days: int = 28,
**kwargs
) -> dict:
"""
[Beschreibung der Daten]
Args:
profile_id: User profile ID
days: Analysis window
**kwargs: Additional parameters
Returns:
{
"<field>": <value>,
"confidence": str, # ALWAYS include!
"data_points": int, # ALWAYS include!
}
"""
with get_db() as conn:
cur = get_cursor(conn)
# 1. DATA RETRIEVAL
cur.execute("""...""", (profile_id,))
rows = cur.fetchall()
# 2. CONFIDENCE CALCULATION
from data_layer.utils import calculate_confidence
confidence = calculate_confidence(
len(rows),
days,
"general" # or "correlation" or "trend"
)
# 3. VALIDATION
if confidence == 'insufficient':
return {
"confidence": "insufficient",
"data_points": len(rows),
# Return minimal safe data
}
# 4. CALCULATION
# ... your logic here ...
# 5. RETURN STRUCTURED DATA
return {
# All data as primitives: dict, list, float, int, str, bool
# NO FORMATTING (no "0.23 kg/Woche" - just 0.23)
"result": result_value,
"confidence": confidence,
"data_points": len(rows),
}
WICHTIG:
- ❌ Keine Strings mit Einheiten:
"0.23 kg/Woche" - ✅ Nur Zahlen:
0.23 - ❌ Keine Formatierung für Menschen
- ✅ Strukturierte Daten für Maschinen
2. KI Layer Wrapper erstellen
Datei: backend/placeholder_resolver.py
from data_layer.<module> import get_<metric>_data
def resolve_<placeholder_name>(profile_id: str) -> str:
"""
[Beschreibung was zurückgegeben wird]
Phase 0c: Uses data_layer.<module>.get_<metric>_data()
"""
data = get_<metric>_data(profile_id)
if data['confidence'] == 'insufficient':
return "Nicht genug Daten"
# FORMAT for KI consumption
return f"{data['<field>']:.2f} <unit>"
3. In Mapping registrieren
UNVERÄNDERT - gleich wie Phase 0b:
PLACEHOLDER_FUNCTIONS = {
"my_new_metric": resolve_my_new_metric,
}
4. In Katalog dokumentieren
UNVERÄNDERT - gleich wie Phase 0b:
def get_placeholder_catalog(profile_id: str):
placeholders = {
'Körper': [
('my_new_metric', 'Beschreibung'),
],
}
5. Testen
Unit Test für Data Layer:
# backend/tests/test_data_layer.py
def test_get_metric_data_sufficient():
data = get_<metric>_data("test_profile_1", days=28)
assert data['confidence'] in ['high', 'medium', 'low', 'insufficient']
assert 'data_points' in data
assert isinstance(data['<field>'], float)
def test_get_metric_data_insufficient():
data = get_<metric>_data("profile_no_data", days=28)
assert data['confidence'] == 'insufficient'
Integration Test für KI Layer:
# backend/tests/test_placeholders.py
def test_resolve_placeholder():
result = resolve_<placeholder_name>("test_profile_1")
assert isinstance(result, str)
assert result != "Nicht genug Daten"
Best Practices
1. Confidence Scoring
IMMER calculate_confidence() verwenden:
from data_layer.utils import calculate_confidence
confidence = calculate_confidence(
data_points=len(rows),
days_requested=days,
metric_type="general" # or "correlation" or "trend"
)
Confidence Thresholds:
- General (28d): high >= 18, medium >= 12, low >= 8
- Correlation: high >= 28, medium >= 21, low >= 14
- Trend: high >= (days * 0.7), medium >= (days * 0.5)
2. Decimal → Float Conversion
PostgreSQL gibt Decimal zurück - immer zu float konvertieren:
# ❌ WRONG:
value = row['column']
# ✅ CORRECT:
value = float(row['column']) if row['column'] else 0.0
3. Safe Dict Access
Nie direkter Key-Zugriff ohne Fallback:
# ❌ WRONG:
value = data['key'] # KeyError if missing
# ✅ CORRECT:
value = data.get('key', default_value)
4. Date Serialization
Python date objects sind nicht JSON-serializable:
from data_layer.utils import serialize_dates
data = {
"date": date(2026, 3, 28),
"values": [...]
}
# Serialize before returning from API
return serialize_dates(data)
5. SQL Parameter Binding
IMMER Parameter-Binding, NIE String-Concatenation:
# ✅ CORRECT:
cur.execute("SELECT * FROM t WHERE id = %s", (id,))
# ❌ WRONG (SQL Injection Risk):
cur.execute(f"SELECT * FROM t WHERE id = {id}")
6. Column Name Consistency
Prüfe Schema BEVOR du Column-Namen verwendest:
# ❌ WRONG (assumed name):
SELECT bf_jpl FROM caliper_log
# ✅ CORRECT (check schema first):
SELECT body_fat_pct FROM caliper_log
Schema prüfen:
\d caliper_log -- in psql
-- oder
SELECT column_name FROM information_schema.columns
WHERE table_name = 'caliper_log';
Fehler-Handling
1. Insufficient Data
Return-Value bei zu wenig Daten:
# Data Layer:
return {
"confidence": "insufficient",
"data_points": 0,
# Alle anderen Felder mit safe defaults (0.0, [], etc.)
}
# KI Layer:
if data['confidence'] == 'insufficient':
return "Nicht genug Daten"
2. Missing Optional Data
Wenn optionale Daten fehlen (z.B. keine Vitals):
# Data Layer:
return {
"hrv": None, # or 0.0, depending on semantic
"confidence": "low", # downgrade confidence
}
# KI Layer:
if data['hrv'] is None:
return "Keine HRV-Daten verfügbar"
3. Calculation Errors
Bei Math-Errors (Division by Zero, etc.):
try:
result = numerator / denominator
except ZeroDivisionError:
result = 0.0 # or None, depending on semantic
Dokumentations-Pflicht
1. Docstring
Jede Funktion braucht Docstring:
def get_metric_data(profile_id: str, days: int = 28) -> dict:
"""
[Eine Zeile Zusammenfassung]
[Ausführliche Beschreibung wenn nötig]
Args:
profile_id: User profile ID
days: Analysis window (default 28)
Returns:
{
"field": value,
"confidence": str,
"data_points": int
}
Confidence Rules:
- high: >= X points
- medium: >= Y points
- low: >= Z points
- insufficient: < Z points
"""
2. Inline Comments
Nur bei nicht-offensichtlicher Logik:
# Calculate trimmed mean (remove top/bottom 10%)
sorted_values = sorted(values)
trim_count = len(values) // 10
trimmed = sorted_values[trim_count:-trim_count]
result = sum(trimmed) / len(trimmed)
3. Type Hints
IMMER Type Hints verwenden:
from typing import Optional, List, Dict, Tuple
def get_data(
profile_id: str,
days: int = 28,
include_raw: bool = False
) -> Dict[str, any]:
...
Testing-Strategie
1. Unit Tests (Data Layer)
Teste jede Data Layer Funktion isoliert:
# backend/tests/test_data_layer.py
import pytest
from data_layer.body_metrics import get_weight_trend_data
@pytest.fixture
def test_profile():
# Setup test data in database
...
yield profile_id
# Teardown
...
def test_weight_trend_sufficient_data(test_profile):
data = get_weight_trend_data(test_profile, days=28)
assert data['confidence'] in ['high', 'medium']
assert data['slope_28d'] != 0.0
assert len(data['raw_values']) >= 18
def test_weight_trend_insufficient_data():
data = get_weight_trend_data("no_data_profile", days=28)
assert data['confidence'] == 'insufficient'
2. Integration Tests (KI Layer)
Teste Placeholder-Resolution:
# backend/tests/test_placeholders.py
def test_placeholder_resolution(test_profile):
result = resolve_weight_28d_trend_slope(test_profile)
assert isinstance(result, str)
assert "kg/Woche" in result or "Nicht genug Daten" in result
def test_placeholder_in_template(test_profile):
template = "Trend: {{weight_28d_trend_slope}}"
result = resolve_placeholders(template, test_profile)
assert "{{" not in result # All placeholders resolved
assert result.startswith("Trend:")
3. Manual Testing Checklist
[ ] Funktion mit verschiedenen days-Parametern testen
[ ] Mit vollständigen Daten testen
[ ] Mit unvollständigen Daten testen
[ ] Mit NO DATA testen
[ ] Edge Cases: Extreme Werte, Outliers
[ ] Performance: < 500ms für typische Queries
[ ] Memory: Kein Leak bei großen Datasets
Checkliste: Neuer Platzhalter
Phase 0b (Aktuell):
[ ] Funktion in placeholder_resolver.py implementiert
[ ] resolve_<name>(profile_id: str) -> str Signatur
[ ] Docstring vollständig
[ ] Confidence-Check implementiert
[ ] In PLACEHOLDER_FUNCTIONS registriert
[ ] In get_placeholder_catalog() dokumentiert
[ ] Manuell getestet
[ ] In echtem Prompt getestet
Phase 0c (Nach Refactoring):
[ ] Data Layer Funktion implementiert
[ ] Richtiges Modul gewählt
[ ] get_<metric>_data(profile_id, ...) -> dict Signatur
[ ] Returns structured data (dict/list/primitives)
[ ] NO formatting, NO strings with units
[ ] Confidence calculation included
[ ] Docstring vollständig
[ ] KI Layer Wrapper implementiert
[ ] resolve_<name>(profile_id: str) -> str Signatur
[ ] Calls data_layer function
[ ] Formats result for KI
[ ] In PLACEHOLDER_FUNCTIONS registriert
[ ] In get_placeholder_catalog() dokumentiert
[ ] Unit Test für Data Layer geschrieben
[ ] Integration Test für KI Layer geschrieben
[ ] Manual Testing durchgeführt
Häufige Fehler (Learnings from Phase 0b)
1. Vergessen float() Conversion
# SYMPTOM: "Object of type Decimal is not JSON serializable"
# FIX:
value = float(row['column']) if row['column'] else 0.0
2. Hardcoded Column Names
# SYMPTOM: "column bf_jpl does not exist"
# FIX: Check schema first
SELECT column_name FROM information_schema.columns
WHERE table_name = 'caliper_log';
3. KeyError bei fehlenden Daten
# SYMPTOM: "KeyError: 'hrv'"
# FIX: Use .get() with default
hrv = data.get('hrv', 0.0)
4. Confidence nicht berechnet
# SYMPTOM: Platzhalter liefert Daten bei <3 Punkten
# FIX: calculate_confidence() verwenden
from data_layer.utils import calculate_confidence
confidence = calculate_confidence(len(rows), days, "general")
5. Date nicht serialized
# SYMPTOM: "Object of type date is not JSON serializable"
# FIX:
from data_layer.utils import serialize_dates
return serialize_dates(data)
6. SQL Injection Risk
# SYMPTOM: Security Scanner warnt
# FIX: ALWAYS use parameter binding
cur.execute("SELECT * FROM t WHERE id = %s", (id,))
Nächste Schritte
Nach Implementierung eines neuen Platzhalters:
-
Commit Message:
feat: add {{my_new_metric}} placeholder - Implements resolve_my_new_metric() in placeholder_resolver.py - Adds entry to PLACEHOLDER_FUNCTIONS - Documents in get_placeholder_catalog() - Tested with profile XYZ Category: <Körper/Ernährung/Training/etc.> Returns: <description> -
Dokumentation aktualisieren:
CLAUDE.md- Neue Platzhalter auflistendocs/api/PLACEHOLDERS.md- API-Dokumentation
-
Testing:
- Mindestens 1 manueller Test mit echtem Profil
- Optional: Unit Test hinzufügen
-
Review:
- Prüfe ob Platzhalter in Prompt-Bibliothek sinnvoll
- Teste mit verschiedenen Prompts
- Performance-Check (< 500ms)
Autor: Claude Sonnet 4.5 Version: 1.0 Letzte Aktualisierung: 28. März 2026