mitai-jinkendo/.claude/docs/technical/PLACEHOLDER_DEVELOPMENT_GUIDE.md
Lars 7940dc7560 docs: Struktur .claude/docs versionieren, working/, Gitea-Index, Regeln
- .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
2026-04-08 13:01:49 +02:00

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_id als Parameter
  • Immer str als 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:

  • Profil
  • Körper
  • Ernährung
  • Training
  • Schlaf & Erholung
  • Vitalwerte
  • Scores (Phase 0b)
  • Focus Areas
  • Zeitraum

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:

  1. 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>
    
  2. Dokumentation aktualisieren:

    • CLAUDE.md - Neue Platzhalter auflisten
    • docs/api/PLACEHOLDERS.md - API-Dokumentation
  3. Testing:

    • Mindestens 1 manueller Test mit echtem Profil
    • Optional: Unit Test hinzufügen
  4. 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