Merge pull request 'develop' (#59) from develop into main
All checks were successful
Deploy Production / deploy (push) Successful in 53s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s

Reviewed-on: #59
This commit is contained in:
Lars 2026-04-03 08:23:48 +02:00
commit 5bbea036c2
27 changed files with 4523 additions and 21 deletions

View File

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

View File

@ -575,22 +575,23 @@ def calculate_protein_g_per_kg(profile_id: str) -> Optional[float]:
weight = float(weight_row['weight'])
# Get protein intake
# Get protein intake aggregated by day (SUM per day)
cur.execute("""
SELECT protein_g
SELECT date, SUM(protein_g) as daily_protein
FROM nutrition_log
WHERE profile_id = %s
AND date >= CURRENT_DATE - INTERVAL '7 days'
AND protein_g IS NOT NULL
GROUP BY date
ORDER BY date DESC
""", (profile_id,))
protein_values = [row['protein_g'] for row in cur.fetchall()]
daily_protein = [float(row['daily_protein']) for row in cur.fetchall()]
if len(protein_values) < 4:
if len(daily_protein) < 4: # At least 4 days with data
return None
avg_protein = float(sum(protein_values) / len(protein_values))
avg_protein = sum(daily_protein) / len(daily_protein)
protein_per_kg = avg_protein / weight
return round(protein_per_kg, 2)
@ -619,28 +620,33 @@ def calculate_protein_days_in_target(profile_id: str, target_low: float = 1.6, t
weight = float(weight_row['weight'])
# Get protein intake last 7 days
# Calculate protein target range (absolute values)
target_low_g = target_low * weight
target_high_g = target_high * weight
# Get protein intake aggregated by day (SUM per day)
cur.execute("""
SELECT protein_g, date
SELECT date, SUM(protein_g) as daily_protein
FROM nutrition_log
WHERE profile_id = %s
AND date >= CURRENT_DATE - INTERVAL '7 days'
AND protein_g IS NOT NULL
GROUP BY date
ORDER BY date DESC
""", (profile_id,))
protein_data = cur.fetchall()
daily_data = cur.fetchall()
if len(protein_data) < 4:
if len(daily_data) < 4: # At least 4 days with data
return None
# Count days in target range
days_in_target = 0
total_days = len(protein_data)
total_days = len(daily_data)
for row in protein_data:
protein_per_kg = float(row['protein_g']) / weight
if target_low <= protein_per_kg <= target_high:
for row in daily_data:
daily_protein = float(row['daily_protein'])
if target_low_g <= daily_protein <= target_high_g:
days_in_target += 1
return f"{days_in_target}/{total_days}"

View File

@ -0,0 +1,14 @@
"""
Placeholder Registrations Package
Auto-imports all placeholder registrations to populate the global registry.
"""
# Import all registration modules to trigger auto-registration
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', 'activity_metrics']

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,216 @@
"""
Nutrition Part A Placeholder Registrations
Registers the 4 basis nutrition metrics in the central placeholder registry:
- kcal_avg
- protein_avg
- carb_avg
- fat_avg
Evidence-based metadata with clear tagging of source.
"""
from placeholder_registry import (
PlaceholderMetadata,
MissingValuePolicy,
EvidenceType,
OutputType,
PlaceholderType,
register_placeholder
)
def register_nutrition_part_a():
"""
Register Part A nutrition placeholders.
Metadata sources:
- code-derived: extracted from actual code
- draft-derived: from canonical requirements draft
- mixed: combination of code and draft
- unresolved: not explicitly documented
- to_verify: claimed but not verified
"""
# Common metadata for all 4 placeholders
common_metadata = {
"category": "Ernährung",
"resolver_module": "backend/placeholder_resolver.py",
"resolver_function": "get_nutrition_avg",
"data_layer_module": "backend/data_layer/nutrition_metrics.py",
"data_layer_function": "get_nutrition_average_data",
"source_tables": ["nutrition_log"],
"time_window": "30d",
"output_type": OutputType.NUMERIC,
"placeholder_type": PlaceholderType.INTERPRETED,
"confidence_logic": "datenpunktbasierte Coverage-Logik (calculate_confidence)",
"missing_value_policy": MissingValuePolicy(
available=False,
value_raw=None,
missing_reason="insufficient_data",
legacy_display="nicht genug Daten"
),
"layer_1_decision": "Data Layer (nutrition_metrics.get_nutrition_average_data)",
"layer_2a_decision": "Placeholder Resolver (formatting only)",
"architecture_alignment": "Phase 0c Multi-Layer Architecture conform",
}
# Common evidence for shared fields
common_evidence = {
"category": EvidenceType.CODE_DERIVED, # from placeholder_resolver.py:1380
"resolver_module": EvidenceType.CODE_DERIVED,
"resolver_function": EvidenceType.CODE_DERIVED,
"data_layer_module": EvidenceType.CODE_DERIVED, # from import statement
"data_layer_function": EvidenceType.CODE_DERIVED, # from resolver code
"source_tables": EvidenceType.CODE_DERIVED, # from SQL query
"time_window": EvidenceType.CODE_DERIVED, # from PLACEHOLDER_MAP lambda
"output_type": EvidenceType.CODE_DERIVED, # from resolver return type
"placeholder_type": EvidenceType.MIXED, # draft classification + code shows aggregation
"confidence_logic": EvidenceType.CODE_DERIVED, # from data layer
"missing_value_policy": EvidenceType.CODE_DERIVED, # from resolver code
"layer_1_decision": EvidenceType.CODE_DERIVED,
"layer_2a_decision": EvidenceType.CODE_DERIVED,
"layer_2b_reuse_possible": EvidenceType.TO_VERIFY, # not verified in charts
"architecture_alignment": EvidenceType.CODE_DERIVED, # imports from data_layer
"issue_53_alignment": EvidenceType.MIXED, # layer separation visible, issue conformity derived
"minimum_data_requirements": EvidenceType.UNRESOLVED, # not explicit in code
"quality_filter_policy": EvidenceType.UNRESOLVED, # not implemented
}
# ── kcal_avg ──────────────────────────────────────────────────────────────
kcal_metadata = PlaceholderMetadata(
key="kcal_avg",
description="Durchschn. Kalorien (30d)",
semantic_contract=(
"Liefert den Durchschnitt der dokumentierten täglichen Kalorienaufnahme "
"über das definierte Auswertungsfenster. Der Wert ist als Intake-Mittelwert "
"zu interpretieren, nicht als Energiebedarf oder Energiebilanz."
),
business_meaning="Kernwert für Ernährungsstatus, Defizit-/Überschussbewertung und Zielabgleich",
unit="kcal/day",
format_hint="Ganzzahl",
example_output="2140",
known_limitations="nur Intake, kein Bedarf; sagt allein nichts über Zielpassung",
layer_2b_reuse_possible=None, # to_verify - not checked in chart code
issue_53_alignment="Layer separation established",
minimum_data_requirements=None, # unresolved
quality_filter_policy=None, # unresolved
**common_metadata
)
kcal_metadata.evidence.update(common_evidence)
kcal_metadata.set_evidence("semantic_contract", EvidenceType.DRAFT_DERIVED)
kcal_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED)
kcal_metadata.set_evidence("unit", EvidenceType.CODE_DERIVED) # from resolver: no " g" suffix
kcal_metadata.set_evidence("format_hint", EvidenceType.CODE_DERIVED) # int(value)
kcal_metadata.set_evidence("example_output", EvidenceType.CODE_DERIVED) # runtime testable
kcal_metadata.set_evidence("known_limitations", EvidenceType.DRAFT_DERIVED)
register_placeholder(kcal_metadata)
# ── protein_avg ───────────────────────────────────────────────────────────
protein_metadata = PlaceholderMetadata(
key="protein_avg",
description="Durchschn. Protein in g (30d)",
semantic_contract=(
"Liefert den Durchschnitt der dokumentierten täglichen Proteinzufuhr "
"über das definierte Auswertungsfenster."
),
business_meaning=(
"Zentraler Placeholder für Muskelerhalt, Muskelaufbau, Recomposition "
"und Absicherung im Defizit"
),
unit="g/day",
format_hint="Ganzzahl in g/day",
example_output="156",
known_limitations=(
"absoluter Wert allein reicht nicht immer; sollte oft relativ zum "
"Körpergewicht interpretiert werden"
),
layer_2b_reuse_possible=None,
issue_53_alignment="Layer separation established",
minimum_data_requirements=None,
quality_filter_policy=None,
**common_metadata
)
protein_metadata.evidence.update(common_evidence)
protein_metadata.set_evidence("semantic_contract", EvidenceType.DRAFT_DERIVED)
protein_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED)
protein_metadata.set_evidence("unit", EvidenceType.CODE_DERIVED) # from resolver: " g" suffix
protein_metadata.set_evidence("format_hint", EvidenceType.CODE_DERIVED)
protein_metadata.set_evidence("example_output", EvidenceType.CODE_DERIVED)
protein_metadata.set_evidence("known_limitations", EvidenceType.DRAFT_DERIVED)
register_placeholder(protein_metadata)
# ── carb_avg ──────────────────────────────────────────────────────────────
carb_metadata = PlaceholderMetadata(
key="carb_avg",
description="Durchschn. Kohlenhydrate in g (30d)",
semantic_contract=(
"Liefert den Durchschnitt der dokumentierten täglichen Kohlenhydratzufuhr "
"über das definierte Auswertungsfenster."
),
business_meaning="Relevanter Makroindikator für Leistungs-, Energie- und Belastungskontext",
unit="g/day",
format_hint="Ganzzahl in g/day",
example_output="210",
known_limitations=(
"allein selten aussagekräftig; meist im Kontext von Ziel, Energie und "
"Belastung relevant"
),
layer_2b_reuse_possible=None,
issue_53_alignment="Layer separation established",
minimum_data_requirements=None,
quality_filter_policy=None,
**common_metadata
)
carb_metadata.evidence.update(common_evidence)
carb_metadata.set_evidence("semantic_contract", EvidenceType.DRAFT_DERIVED)
carb_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED)
carb_metadata.set_evidence("unit", EvidenceType.CODE_DERIVED)
carb_metadata.set_evidence("format_hint", EvidenceType.CODE_DERIVED)
carb_metadata.set_evidence("example_output", EvidenceType.CODE_DERIVED)
carb_metadata.set_evidence("known_limitations", EvidenceType.DRAFT_DERIVED)
register_placeholder(carb_metadata)
# ── fat_avg ───────────────────────────────────────────────────────────────
fat_metadata = PlaceholderMetadata(
key="fat_avg",
description="Durchschn. Fett in g (30d)",
semantic_contract=(
"Liefert den Durchschnitt der dokumentierten täglichen Fettzufuhr "
"über das definierte Auswertungsfenster."
),
business_meaning="Relevanter Makroindikator für Ernährungsstruktur und Zielpassung",
unit="g/day",
format_hint="Ganzzahl in g/day",
example_output="72",
known_limitations="meist im Gesamtkontext der Makroverteilung relevant",
layer_2b_reuse_possible=None,
issue_53_alignment="Layer separation established",
minimum_data_requirements=None,
quality_filter_policy=None,
**common_metadata
)
fat_metadata.evidence.update(common_evidence)
fat_metadata.set_evidence("semantic_contract", EvidenceType.DRAFT_DERIVED)
fat_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED)
fat_metadata.set_evidence("unit", EvidenceType.CODE_DERIVED)
fat_metadata.set_evidence("format_hint", EvidenceType.CODE_DERIVED)
fat_metadata.set_evidence("example_output", EvidenceType.CODE_DERIVED)
fat_metadata.set_evidence("known_limitations", EvidenceType.DRAFT_DERIVED)
register_placeholder(fat_metadata)
# Auto-register on import
register_nutrition_part_a()

View File

@ -0,0 +1,429 @@
"""
Nutrition Part B Placeholder Registrations
Registers the 5 protein-specific metrics in the central placeholder registry:
- protein_ziel_low
- protein_ziel_high
- protein_g_per_kg
- protein_days_in_target
- protein_adequacy_28d
Evidence-based metadata with clear tagging of source.
Includes documentation of open points (weight basis inconsistency, score logic).
"""
from placeholder_registry import (
PlaceholderMetadata,
MissingValuePolicy,
EvidenceType,
OutputType,
PlaceholderType,
register_placeholder
)
def register_nutrition_part_b():
"""
Register Part B protein placeholders.
Metadata sources:
- code-derived: extracted from actual code
- draft-derived: from canonical requirements draft
- mixed: combination of code and draft
- unresolved: not explicitly documented
- to_verify: claimed but not verified
"""
# ── protein_ziel_low ──────────────────────────────────────────────────────
low_metadata = PlaceholderMetadata(
key="protein_ziel_low",
category="Ernährung",
description="Unteres Proteinziel (1.6 g/kg)",
# Technical
resolver_module="backend/placeholder_resolver.py",
resolver_function="get_protein_ziel_low",
data_layer_module="backend/data_layer/nutrition_metrics.py",
data_layer_function="get_protein_targets_data",
source_tables=["weight_log"],
# Semantic
semantic_contract=(
"Liefert die untere Proteinziel-Grenze basierend auf aktuellem "
"Körpergewicht (1.6 g/kg). Ziel für Muskelerhalt in Maintenance-Phasen."
),
business_meaning="Maintenance-Ziel für Muskelerhalt",
unit="g/day",
time_window="snapshot",
output_type=OutputType.NUMERIC,
placeholder_type=PlaceholderType.INTERPRETED,
format_hint="Ganzzahl",
example_output="128",
# Quality
confidence_logic="Binary: weight vorhanden/nicht vorhanden",
missing_value_policy=MissingValuePolicy(
available=False,
value_raw=None,
missing_reason="no_data",
legacy_display="nicht verfügbar"
),
known_limitations=(
"Basiert auf single-point weight (latest entry); "
"anfällig für Gewichts-Outlier (z.B. nach Refeed-Tag)"
),
# Architecture
layer_1_decision="Data Layer (nutrition_metrics.get_protein_targets_data)",
layer_2a_decision="Placeholder Resolver (formatting only)",
layer_2b_reuse_possible=None, # to_verify
architecture_alignment="Phase 0c Multi-Layer Architecture conform",
issue_53_alignment="Layer separation established"
)
# Evidence
low_metadata.set_evidence("key", EvidenceType.CODE_DERIVED)
low_metadata.set_evidence("category", EvidenceType.CODE_DERIVED)
low_metadata.set_evidence("description", EvidenceType.MIXED)
low_metadata.set_evidence("resolver_module", EvidenceType.CODE_DERIVED)
low_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED)
low_metadata.set_evidence("data_layer_module", EvidenceType.CODE_DERIVED)
low_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED)
low_metadata.set_evidence("source_tables", EvidenceType.CODE_DERIVED)
low_metadata.set_evidence("semantic_contract", EvidenceType.DRAFT_DERIVED)
low_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED)
low_metadata.set_evidence("unit", EvidenceType.MIXED) # implicit in code, confirmed by draft
low_metadata.set_evidence("time_window", EvidenceType.CODE_DERIVED)
low_metadata.set_evidence("output_type", EvidenceType.CODE_DERIVED)
low_metadata.set_evidence("placeholder_type", EvidenceType.MIXED)
low_metadata.set_evidence("format_hint", EvidenceType.CODE_DERIVED)
low_metadata.set_evidence("example_output", EvidenceType.CODE_DERIVED)
low_metadata.set_evidence("confidence_logic", EvidenceType.CODE_DERIVED)
low_metadata.set_evidence("missing_value_policy", EvidenceType.CODE_DERIVED)
low_metadata.set_evidence("known_limitations", EvidenceType.MIXED)
low_metadata.set_evidence("layer_1_decision", EvidenceType.CODE_DERIVED)
low_metadata.set_evidence("layer_2a_decision", EvidenceType.CODE_DERIVED)
low_metadata.set_evidence("layer_2b_reuse_possible", EvidenceType.TO_VERIFY)
low_metadata.set_evidence("architecture_alignment", EvidenceType.CODE_DERIVED)
low_metadata.set_evidence("issue_53_alignment", EvidenceType.MIXED)
register_placeholder(low_metadata)
# ── protein_ziel_high ─────────────────────────────────────────────────────
high_metadata = PlaceholderMetadata(
key="protein_ziel_high",
category="Ernährung",
description="Oberes Proteinziel (2.2 g/kg)",
# Technical (same as protein_ziel_low)
resolver_module="backend/placeholder_resolver.py",
resolver_function="get_protein_ziel_high",
data_layer_module="backend/data_layer/nutrition_metrics.py",
data_layer_function="get_protein_targets_data",
source_tables=["weight_log"],
# Semantic
semantic_contract=(
"Liefert die obere Proteinziel-Grenze basierend auf aktuellem "
"Körpergewicht (2.2 g/kg). Ziel für Muskelaufbau in hypertrophen Phasen."
),
business_meaning="Muskelaufbau-Ziel für hypertrophe Phasen",
unit="g/day",
time_window="snapshot",
output_type=OutputType.NUMERIC,
placeholder_type=PlaceholderType.INTERPRETED,
format_hint="Ganzzahl",
example_output="176",
# Quality
confidence_logic="Binary: weight vorhanden/nicht vorhanden",
missing_value_policy=MissingValuePolicy(
available=False,
value_raw=None,
missing_reason="no_data",
legacy_display="nicht verfügbar"
),
known_limitations=(
"Basiert auf single-point weight (latest entry); "
"anfällig für Gewichts-Outlier"
),
# Architecture
layer_1_decision="Data Layer (nutrition_metrics.get_protein_targets_data)",
layer_2a_decision="Placeholder Resolver (formatting only)",
layer_2b_reuse_possible=None,
architecture_alignment="Phase 0c Multi-Layer Architecture conform",
issue_53_alignment="Layer separation established"
)
# Evidence (identical to protein_ziel_low)
high_metadata.set_evidence("key", EvidenceType.CODE_DERIVED)
high_metadata.set_evidence("category", EvidenceType.CODE_DERIVED)
high_metadata.set_evidence("description", EvidenceType.MIXED)
high_metadata.set_evidence("resolver_module", EvidenceType.CODE_DERIVED)
high_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED)
high_metadata.set_evidence("data_layer_module", EvidenceType.CODE_DERIVED)
high_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED)
high_metadata.set_evidence("source_tables", EvidenceType.CODE_DERIVED)
high_metadata.set_evidence("semantic_contract", EvidenceType.DRAFT_DERIVED)
high_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED)
high_metadata.set_evidence("unit", EvidenceType.MIXED)
high_metadata.set_evidence("time_window", EvidenceType.CODE_DERIVED)
high_metadata.set_evidence("output_type", EvidenceType.CODE_DERIVED)
high_metadata.set_evidence("placeholder_type", EvidenceType.MIXED)
high_metadata.set_evidence("format_hint", EvidenceType.CODE_DERIVED)
high_metadata.set_evidence("example_output", EvidenceType.CODE_DERIVED)
high_metadata.set_evidence("confidence_logic", EvidenceType.CODE_DERIVED)
high_metadata.set_evidence("missing_value_policy", EvidenceType.CODE_DERIVED)
high_metadata.set_evidence("known_limitations", EvidenceType.MIXED)
high_metadata.set_evidence("layer_1_decision", EvidenceType.CODE_DERIVED)
high_metadata.set_evidence("layer_2a_decision", EvidenceType.CODE_DERIVED)
high_metadata.set_evidence("layer_2b_reuse_possible", EvidenceType.TO_VERIFY)
high_metadata.set_evidence("architecture_alignment", EvidenceType.CODE_DERIVED)
high_metadata.set_evidence("issue_53_alignment", EvidenceType.MIXED)
register_placeholder(high_metadata)
# ── protein_g_per_kg ──────────────────────────────────────────────────────
gpk_metadata = PlaceholderMetadata(
key="protein_g_per_kg",
category="Ernährung",
description="Protein g/kg Körpergewicht",
# Technical
resolver_module="backend/placeholder_resolver.py",
resolver_function="_safe_float",
data_layer_module="backend/data_layer/nutrition_metrics.py",
data_layer_function="calculate_protein_g_per_kg",
source_tables=["nutrition_log", "weight_log"],
# Semantic
semantic_contract=(
"Liefert die durchschnittliche Proteinzufuhr relativ zum Körpergewicht. "
"Berechnung: protein_7d_avg / latest_weight. "
"WICHTIG: Protein ist geglättet (7d), Gewicht ist single-point."
),
business_meaning="Zentraler Zielindikator für Muskelerhalt und Aufbau",
unit="g/kg/day",
time_window="7d", # dominante Komponente (protein); weight ist snapshot, aber secondary
output_type=OutputType.NUMERIC,
placeholder_type=PlaceholderType.INTERPRETED,
format_hint="Dezimalzahl (1-2 Stellen)",
example_output="1.95",
# Quality
confidence_logic="Minimum von protein_confidence und weight_availability",
missing_value_policy=MissingValuePolicy(
available=False,
value_raw=None,
missing_reason="insufficient_data",
legacy_display="nicht verfügbar"
),
known_limitations=(
"KRITISCHE INKONSISTENZ: Protein ist geglättet (7d average), "
"Gewicht ist single-point (latest). Anfällig für Gewichts-Outlier. "
"Ein Refeed-Tag kann den Wert stark verfälschen, obwohl Protein-Intake stabil ist."
),
# Architecture
layer_1_decision="Data Layer (nutrition_metrics.calculate_protein_g_per_kg)",
layer_2a_decision="Placeholder Resolver (_safe_float wrapper)",
layer_2b_reuse_possible=None,
architecture_alignment="Phase 0c Multi-Layer Architecture conform",
issue_53_alignment="Layer separation established"
)
# Evidence
gpk_metadata.set_evidence("key", EvidenceType.CODE_DERIVED)
gpk_metadata.set_evidence("category", EvidenceType.CODE_DERIVED)
gpk_metadata.set_evidence("description", EvidenceType.CODE_DERIVED)
gpk_metadata.set_evidence("resolver_module", EvidenceType.CODE_DERIVED)
gpk_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED)
gpk_metadata.set_evidence("data_layer_module", EvidenceType.CODE_DERIVED)
gpk_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED)
gpk_metadata.set_evidence("source_tables", EvidenceType.CODE_DERIVED)
gpk_metadata.set_evidence("semantic_contract", EvidenceType.MIXED) # code + explicit documentation
gpk_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED)
gpk_metadata.set_evidence("unit", EvidenceType.CODE_DERIVED)
gpk_metadata.set_evidence("time_window", EvidenceType.CODE_DERIVED) # explicitly documented as mixed
gpk_metadata.set_evidence("output_type", EvidenceType.CODE_DERIVED)
gpk_metadata.set_evidence("placeholder_type", EvidenceType.MIXED)
gpk_metadata.set_evidence("format_hint", EvidenceType.CODE_DERIVED)
gpk_metadata.set_evidence("example_output", EvidenceType.CODE_DERIVED)
gpk_metadata.set_evidence("confidence_logic", EvidenceType.UNRESOLVED) # not explicitly documented
gpk_metadata.set_evidence("missing_value_policy", EvidenceType.CODE_DERIVED)
gpk_metadata.set_evidence("known_limitations", EvidenceType.CODE_DERIVED) # identified from code analysis
gpk_metadata.set_evidence("layer_1_decision", EvidenceType.CODE_DERIVED)
gpk_metadata.set_evidence("layer_2a_decision", EvidenceType.CODE_DERIVED)
gpk_metadata.set_evidence("layer_2b_reuse_possible", EvidenceType.TO_VERIFY)
gpk_metadata.set_evidence("architecture_alignment", EvidenceType.CODE_DERIVED)
gpk_metadata.set_evidence("issue_53_alignment", EvidenceType.MIXED)
register_placeholder(gpk_metadata)
# ── protein_days_in_target ────────────────────────────────────────────────
days_metadata = PlaceholderMetadata(
key="protein_days_in_target",
category="Ernährung",
description="Tage im Protein-Zielbereich (7d)",
# Technical
resolver_module="backend/placeholder_resolver.py",
resolver_function="_safe_str",
data_layer_module="backend/data_layer/nutrition_metrics.py",
data_layer_function="calculate_protein_days_in_target",
source_tables=["nutrition_log", "weight_log"],
# Semantic
semantic_contract=(
"Liefert Anzahl Tage im Protein-Zielbereich relativ zu Gesamttagen. "
"Target-Range: 1.6-2.2 g/kg (hardcoded). "
"Format: 'X/Y' (z.B. '5/7' = 5 von 7 Tagen im Ziel)."
),
business_meaning="Adhärenz-Indikator für Proteinversorgung",
unit="days_ratio",
time_window="7d",
output_type=OutputType.STRING,
placeholder_type=PlaceholderType.INTERPRETED,
format_hint="String format 'X/Y' (e.g. '5/7')",
example_output="5/7",
# Quality
confidence_logic="Abhängig von nutrition_log Datenabdeckung",
missing_value_policy=MissingValuePolicy(
available=False,
value_raw=None,
missing_reason="no_data",
legacy_display="nicht verfügbar"
),
known_limitations=(
"Target-Range 1.6-2.2 g/kg fest kodiert (default parameters), "
"nicht konfigurierbar. Keine Integration mit Goal-System."
),
# Architecture
layer_1_decision="Data Layer (nutrition_metrics.calculate_protein_days_in_target)",
layer_2a_decision="Placeholder Resolver (_safe_str wrapper)",
layer_2b_reuse_possible=None,
architecture_alignment="Phase 0c Multi-Layer Architecture conform",
issue_53_alignment="Layer separation established"
)
# Evidence
days_metadata.set_evidence("key", EvidenceType.CODE_DERIVED)
days_metadata.set_evidence("category", EvidenceType.CODE_DERIVED)
days_metadata.set_evidence("description", EvidenceType.MIXED)
days_metadata.set_evidence("resolver_module", EvidenceType.CODE_DERIVED)
days_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED)
days_metadata.set_evidence("data_layer_module", EvidenceType.CODE_DERIVED)
days_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED)
days_metadata.set_evidence("source_tables", EvidenceType.CODE_DERIVED)
days_metadata.set_evidence("semantic_contract", EvidenceType.MIXED)
days_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED)
days_metadata.set_evidence("unit", EvidenceType.CODE_DERIVED)
days_metadata.set_evidence("time_window", EvidenceType.CODE_DERIVED)
days_metadata.set_evidence("output_type", EvidenceType.CODE_DERIVED)
days_metadata.set_evidence("placeholder_type", EvidenceType.MIXED)
days_metadata.set_evidence("format_hint", EvidenceType.CODE_DERIVED)
days_metadata.set_evidence("example_output", EvidenceType.CODE_DERIVED)
days_metadata.set_evidence("confidence_logic", EvidenceType.UNRESOLVED)
days_metadata.set_evidence("missing_value_policy", EvidenceType.CODE_DERIVED)
days_metadata.set_evidence("known_limitations", EvidenceType.CODE_DERIVED)
days_metadata.set_evidence("layer_1_decision", EvidenceType.CODE_DERIVED)
days_metadata.set_evidence("layer_2a_decision", EvidenceType.CODE_DERIVED)
days_metadata.set_evidence("layer_2b_reuse_possible", EvidenceType.TO_VERIFY)
days_metadata.set_evidence("architecture_alignment", EvidenceType.CODE_DERIVED)
days_metadata.set_evidence("issue_53_alignment", EvidenceType.MIXED)
register_placeholder(days_metadata)
# ── protein_adequacy_28d ──────────────────────────────────────────────────
adequacy_metadata = PlaceholderMetadata(
key="protein_adequacy_28d",
category="Ernährung",
description="Protein Adequacy Score (0-100)",
# Technical
resolver_module="backend/placeholder_resolver.py",
resolver_function="_safe_int",
data_layer_module="backend/data_layer/nutrition_metrics.py",
data_layer_function="calculate_protein_adequacy_28d",
source_tables=["nutrition_log", "weight_log"],
# Semantic
semantic_contract=(
"Liefert standardisierten Angemessenheitswert der Proteinversorgung "
"über 28 Tage relativ zu definierten Protein-Zielbereichen (1.6-2.2 g/kg). "
"Score-Logik: "
"- Days in target [1.6-2.2]: 100 points; "
"- Days slightly below [1.4-1.6]: partial points (linear interpolation); "
"- Days far below (<1.4): 0 points; "
"- Days above (>2.2): 100 points (no penalty). "
"Final score: average over 28d."
),
business_meaning="Verdichteter Zielerreichungsindikator für Proteinversorgung",
unit="score (0-100)",
time_window="28d",
output_type=OutputType.NUMERIC,
placeholder_type=PlaceholderType.SCORE,
format_hint="Integer 0-100, höher = besser",
example_output="82",
# Quality
confidence_logic="Abgeleitet aus Datenabdeckung über 28d",
missing_value_policy=MissingValuePolicy(
available=False,
value_raw=None,
missing_reason="insufficient_data",
legacy_display="nicht verfügbar"
),
known_limitations=(
"Score muss transparent erklärt werden; ohne Skalen-Dokumentation "
"interpretationsanfällig. Scoring-Schwellen [1.4, 1.6, 2.2] nicht explizit "
"im Code dokumentiert, nur in Logik implementiert."
),
# Architecture
layer_1_decision="Data Layer (nutrition_metrics.calculate_protein_adequacy_28d)",
layer_2a_decision="Placeholder Resolver (_safe_int wrapper)",
layer_2b_reuse_possible=None,
architecture_alignment="Phase 0c Multi-Layer Architecture conform",
issue_53_alignment="Layer separation established"
)
# Evidence
adequacy_metadata.set_evidence("key", EvidenceType.CODE_DERIVED)
adequacy_metadata.set_evidence("category", EvidenceType.CODE_DERIVED)
adequacy_metadata.set_evidence("description", EvidenceType.CODE_DERIVED)
adequacy_metadata.set_evidence("resolver_module", EvidenceType.CODE_DERIVED)
adequacy_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED)
adequacy_metadata.set_evidence("data_layer_module", EvidenceType.CODE_DERIVED)
adequacy_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED)
adequacy_metadata.set_evidence("source_tables", EvidenceType.CODE_DERIVED)
adequacy_metadata.set_evidence("semantic_contract", EvidenceType.MIXED) # code + explicit documentation
adequacy_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED)
adequacy_metadata.set_evidence("unit", EvidenceType.CODE_DERIVED)
adequacy_metadata.set_evidence("time_window", EvidenceType.CODE_DERIVED)
adequacy_metadata.set_evidence("output_type", EvidenceType.CODE_DERIVED)
adequacy_metadata.set_evidence("placeholder_type", EvidenceType.MIXED)
adequacy_metadata.set_evidence("format_hint", EvidenceType.CODE_DERIVED)
adequacy_metadata.set_evidence("example_output", EvidenceType.CODE_DERIVED)
adequacy_metadata.set_evidence("confidence_logic", EvidenceType.UNRESOLVED)
adequacy_metadata.set_evidence("missing_value_policy", EvidenceType.CODE_DERIVED)
adequacy_metadata.set_evidence("known_limitations", EvidenceType.MIXED) # code analysis + draft
adequacy_metadata.set_evidence("layer_1_decision", EvidenceType.CODE_DERIVED)
adequacy_metadata.set_evidence("layer_2a_decision", EvidenceType.CODE_DERIVED)
adequacy_metadata.set_evidence("layer_2b_reuse_possible", EvidenceType.TO_VERIFY)
adequacy_metadata.set_evidence("architecture_alignment", EvidenceType.CODE_DERIVED)
adequacy_metadata.set_evidence("issue_53_alignment", EvidenceType.MIXED)
register_placeholder(adequacy_metadata)
# Auto-register on import
register_nutrition_part_b()

View File

@ -0,0 +1,446 @@
"""
Placeholder Registrations - Nutrition Part C
Registers 5 nutrition-related placeholders with complete metadata:
- macro_consistency_score
- energy_balance_7d
- energy_deficit_surplus
- intake_volatility
- nutrition_days
All placeholders follow Phase 0c Multi-Layer Architecture.
"""
from placeholder_registry import (
PlaceholderMetadata,
MissingValuePolicy,
EvidenceType,
OutputType,
PlaceholderType,
register_placeholder
)
# ═══════════════════════════════════════════════════════════════════════════════
# 1. macro_consistency_score
# ═══════════════════════════════════════════════════════════════════════════════
macro_consistency_metadata = PlaceholderMetadata(
key="macro_consistency_score",
category="Ernährung",
description="Makro-Konsistenz Score (0-100)",
# Technical
resolver_module="backend/placeholder_resolver.py",
resolver_function="_safe_int('macro_consistency_score', pid)",
data_layer_module="backend/data_layer/nutrition_metrics.py",
data_layer_function="calculate_macro_consistency_score",
source_tables=["nutrition_log"],
# Semantic
semantic_contract="Liefert einen standardisierten Score (0-100), der die Stabilität bzw. Varianz der Makronährstoffzufuhr über 28 Tage bewertet. Niedriger CV (Coefficient of Variation) = höherer Score.",
business_meaning="Verdichteter Konsistenzindikator für Ernährungsumsetzung. Score basiert auf durchschnittlicher Variabilität der Makros (kcal, protein, fat, carbs).",
unit="score (0-100)",
time_window="28d",
output_type=OutputType.NUMERIC,
placeholder_type=PlaceholderType.INTERPRETED,
format_hint="Ganzzahl 0-100",
example_output="74",
# Quality
minimum_data_requirements="Mindestens 18 Einträge in 28 Tagen (60% coverage) für verlässliche Varianzberechnung.",
quality_filter_policy="Unvollständige oder stark lückenhafte Tage schwächen Aussagekraft. NULL-Werte bei einzelnen Makros werden für CV-Berechnung übersprungen.",
confidence_logic="Aus Datenabdeckung ableiten: 18+ Einträge = ausreichend für CV-Berechnung. Score selbst ist bereits ein Konsistenzmaß.",
missing_value_policy=MissingValuePolicy(
available=False,
value_raw=None,
missing_reason="insufficient_data",
legacy_display="nicht verfügbar"
),
known_limitations=(
"Score-Formel: CV (Coefficient of Variation) = std_dev / mean für jeden Makro. "
"Durchschnittlicher CV über alle 4 Makros. "
"Thresholds: CV<0.2=100, CV<0.3=85, CV<0.4=70, CV<0.5=55, CV>=0.5=max(30,100-CV*100). "
"WICHTIG: Niedrige Konsistenz ist nicht automatisch schlecht (bewusste Zyklen, Refeed-Tage). "
"Interpretation hängt vom Zielkontext ab."
),
# Architecture
layer_1_decision="Data Layer (nutrition_metrics.calculate_macro_consistency_score)",
layer_2a_decision="Placeholder Resolver (_safe_int, keine zusätzliche Logik)",
layer_2b_reuse_possible="Ja - Chart für Konsistenz-Verlauf oder Score-Trend möglich",
architecture_alignment="Phase 0c conform - Data Layer liefert Score, Resolver formatiert nur",
issue_53_alignment="Konform - Data Layer berechnet, Resolver wraps",
# Evidence (not exported, internal tracking)
evidence={}
)
# Evidence tagging
macro_consistency_metadata.set_evidence("resolver_module", EvidenceType.CODE_DERIVED)
macro_consistency_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED)
macro_consistency_metadata.set_evidence("data_layer_module", EvidenceType.CODE_DERIVED)
macro_consistency_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED)
macro_consistency_metadata.set_evidence("source_tables", EvidenceType.CODE_DERIVED)
macro_consistency_metadata.set_evidence("unit", EvidenceType.CODE_DERIVED)
macro_consistency_metadata.set_evidence("time_window", EvidenceType.CODE_DERIVED)
macro_consistency_metadata.set_evidence("output_type", EvidenceType.CODE_DERIVED)
macro_consistency_metadata.set_evidence("format_hint", EvidenceType.CODE_DERIVED)
macro_consistency_metadata.set_evidence("minimum_data_requirements", EvidenceType.CODE_DERIVED)
macro_consistency_metadata.set_evidence("quality_filter_policy", EvidenceType.CODE_DERIVED)
macro_consistency_metadata.set_evidence("confidence_logic", EvidenceType.CODE_DERIVED)
macro_consistency_metadata.set_evidence("semantic_contract", EvidenceType.DRAFT_DERIVED)
macro_consistency_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED)
macro_consistency_metadata.set_evidence("known_limitations", EvidenceType.MIXED) # Formula from code, interpretation from draft
macro_consistency_metadata.set_evidence("layer_1_decision", EvidenceType.TO_VERIFY)
macro_consistency_metadata.set_evidence("layer_2a_decision", EvidenceType.TO_VERIFY)
macro_consistency_metadata.set_evidence("layer_2b_reuse_possible", EvidenceType.TO_VERIFY)
register_placeholder(macro_consistency_metadata)
# ═══════════════════════════════════════════════════════════════════════════════
# 2. energy_balance_7d
# ═══════════════════════════════════════════════════════════════════════════════
energy_balance_metadata = PlaceholderMetadata(
key="energy_balance_7d",
category="Ernährung",
description="Energiebilanz 7-Tage (kcal/Tag Durchschnitt)",
# Technical
resolver_module="backend/placeholder_resolver.py",
resolver_function="_safe_float('energy_balance_7d', pid, decimals=0)",
data_layer_module="backend/data_layer/nutrition_metrics.py",
data_layer_function="calculate_energy_balance_7d",
source_tables=["nutrition_log", "weight_log"],
# Semantic
semantic_contract="Liefert die geschätzte Energiebilanz über 7 Tage als Differenz zwischen durchschnittlicher Energieaufnahme und geschätztem TDEE (Total Daily Energy Expenditure). Positiver Wert = Überschuss, Negativer Wert = Defizit.",
business_meaning="Kernindikator für Defizit-/Überschussrichtung im Kurzfristfenster. Zeigt, ob aktuelle Ernährung auf Gewichtsverlust, Erhaltung oder Aufbau ausgerichtet ist.",
unit="kcal/day (Durchschnitt)",
time_window="7d",
output_type=OutputType.NUMERIC,
placeholder_type=PlaceholderType.INTERPRETED,
format_hint="Ganzzahl, gerundet auf 0 Dezimalstellen",
example_output="-380",
# Quality
minimum_data_requirements="Mindestens 4 Tage mit Kalorienerfassung in 7-Tage-Fenster. Aktuelles Gewicht aus weight_log erforderlich.",
quality_filter_policy="Unvollständige Intake-Daten und fehlende Gewichtsmessung reduzieren Verlässlichkeit. TDEE-Schätzung ist vereinfacht (weight_kg × 32.5).",
confidence_logic=(
"Kombiniert Intake-Abdeckung und Robustheit des Verbrauchsmodells. "
"Niedrigere Confidence bei <7 Tagen Daten oder fehlendem Gewicht. "
"TDEE-Modell ist vereinfacht → inherent uncertainty."
),
missing_value_policy=MissingValuePolicy(
available=False,
value_raw=None,
missing_reason="insufficient_data",
legacy_display="nicht verfügbar"
),
known_limitations=(
"TDEE-MODELL: Vereinfacht als bodyweight_kg × 32.5 (mittlerer Multiplikator). "
"NICHT berücksichtigt: Aktivitätslevel, Alter, Geschlecht, Stoffwechselanpassungen. "
"TODO in Code: Harris-Benedict oder Mifflin-St Jeor für präzisere TDEE-Schätzung. "
"ACHTUNG: Energiebilanz ist modellbasiert, nicht direkt gemessen. "
"Einheit ist kcal/Tag (daily average), NICHT 7d-Total."
),
# Architecture
layer_1_decision="Data Layer (nutrition_metrics.calculate_energy_balance_7d) - berechnet Balance aus Intake und TDEE",
layer_2a_decision="Placeholder Resolver (_safe_float, rundet auf 0 Dezimalstellen)",
layer_2b_reuse_possible="Ja - Chart für Energiebilanz-Verlauf oder Defizit-Trend",
architecture_alignment="Phase 0c conform - Data Layer berechnet Balance, Resolver formatiert",
issue_53_alignment="Konform - Berechnung in Data Layer",
# Evidence
evidence={}
)
# Evidence tagging
energy_balance_metadata.set_evidence("resolver_module", EvidenceType.CODE_DERIVED)
energy_balance_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED)
energy_balance_metadata.set_evidence("data_layer_module", EvidenceType.CODE_DERIVED)
energy_balance_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED)
energy_balance_metadata.set_evidence("source_tables", EvidenceType.CODE_DERIVED)
energy_balance_metadata.set_evidence("unit", EvidenceType.MIXED) # Code says kcal/day, canonical was ambiguous
energy_balance_metadata.set_evidence("time_window", EvidenceType.CODE_DERIVED)
energy_balance_metadata.set_evidence("output_type", EvidenceType.CODE_DERIVED)
energy_balance_metadata.set_evidence("format_hint", EvidenceType.CODE_DERIVED)
energy_balance_metadata.set_evidence("minimum_data_requirements", EvidenceType.CODE_DERIVED)
energy_balance_metadata.set_evidence("quality_filter_policy", EvidenceType.CODE_DERIVED)
energy_balance_metadata.set_evidence("confidence_logic", EvidenceType.MIXED)
energy_balance_metadata.set_evidence("semantic_contract", EvidenceType.DRAFT_DERIVED)
energy_balance_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED)
energy_balance_metadata.set_evidence("known_limitations", EvidenceType.MIXED) # TDEE formula from code, limitations from both
energy_balance_metadata.set_evidence("layer_1_decision", EvidenceType.TO_VERIFY)
energy_balance_metadata.set_evidence("layer_2a_decision", EvidenceType.TO_VERIFY)
energy_balance_metadata.set_evidence("layer_2b_reuse_possible", EvidenceType.TO_VERIFY)
register_placeholder(energy_balance_metadata)
# ═══════════════════════════════════════════════════════════════════════════════
# 3. energy_deficit_surplus
# ═══════════════════════════════════════════════════════════════════════════════
energy_deficit_surplus_metadata = PlaceholderMetadata(
key="energy_deficit_surplus",
category="Ernährung",
description="Energie Defizit/Überschuss Status",
# Technical
resolver_module="backend/placeholder_resolver.py",
resolver_function="_safe_str('energy_deficit_surplus', pid)",
data_layer_module="backend/data_layer/nutrition_metrics.py",
data_layer_function="calculate_energy_deficit_surplus",
source_tables=["nutrition_log", "weight_log"], # Indirect via energy_balance_7d
# Semantic
semantic_contract="Liefert qualitative Einordnung, ob aktuelle Energiezufuhr relativ zum geschätzten Bedarf in einem Defizit ('deficit'), auf Erhaltung ('maintenance') oder im Überschuss ('surplus') liegt.",
business_meaning="Leicht interpretierbarer Energie-Statusindikator. Vereinfacht Energiebilanz zu verständlichen Kategorien.",
unit="state (string)",
time_window="7d",
output_type=OutputType.STRING,
placeholder_type=PlaceholderType.INTERPRETED,
format_hint="Einer von drei Statuswerten: 'deficit', 'maintenance', 'surplus'",
example_output="deficit",
# Quality
minimum_data_requirements="Wie energy_balance_7d: mindestens 4 Tage mit Kalorienerfassung + aktuelles Gewicht.",
quality_filter_policy="Wie energy_balance_7d: unvollständige Intake-Daten und vereinfachte TDEE-Schätzung reduzieren Verlässlichkeit.",
confidence_logic="Abgeleitet von energy_balance_7d. Confidence der Balance überträgt sich auf Status.",
missing_value_policy=MissingValuePolicy(
available=False,
value_raw=None,
missing_reason="insufficient_data",
legacy_display="nicht verfügbar"
),
known_limitations=(
"Status-Schwellen: balance < -200 kcal/day = 'deficit', "
"balance > +200 kcal/day = 'surplus', "
"-200 bis +200 = 'maintenance'. "
"WICHTIG: Nur so gut wie zugrunde liegende TDEE-Schätzung (siehe energy_balance_7d). "
"Minimale Abweichungen nahe Maintenance-Schwelle können zu Statuswechsel führen. "
"200 kcal Schwelle ist willkürlich gewählt - physiologisch könnten auch 100-300 kcal sinnvoll sein."
),
# Architecture
layer_1_decision="Data Layer (nutrition_metrics.calculate_energy_deficit_surplus) - mapped Balance zu Status",
layer_2a_decision="Placeholder Resolver (_safe_str, keine zusätzliche Logik)",
layer_2b_reuse_possible="Ja - Status-Anzeige oder Kategorien-Chart",
architecture_alignment="Phase 0c conform - Status-Mapping in Data Layer",
issue_53_alignment="Konform - Kategorisierung in Data Layer",
# Evidence
evidence={}
)
# Evidence tagging
energy_deficit_surplus_metadata.set_evidence("resolver_module", EvidenceType.CODE_DERIVED)
energy_deficit_surplus_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED)
energy_deficit_surplus_metadata.set_evidence("data_layer_module", EvidenceType.CODE_DERIVED)
energy_deficit_surplus_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED)
energy_deficit_surplus_metadata.set_evidence("source_tables", EvidenceType.MIXED) # Indirect via energy_balance_7d
energy_deficit_surplus_metadata.set_evidence("unit", EvidenceType.CODE_DERIVED)
energy_deficit_surplus_metadata.set_evidence("time_window", EvidenceType.CODE_DERIVED)
energy_deficit_surplus_metadata.set_evidence("output_type", EvidenceType.CODE_DERIVED)
energy_deficit_surplus_metadata.set_evidence("format_hint", EvidenceType.CODE_DERIVED)
energy_deficit_surplus_metadata.set_evidence("minimum_data_requirements", EvidenceType.MIXED)
energy_deficit_surplus_metadata.set_evidence("quality_filter_policy", EvidenceType.MIXED)
energy_deficit_surplus_metadata.set_evidence("confidence_logic", EvidenceType.MIXED)
energy_deficit_surplus_metadata.set_evidence("semantic_contract", EvidenceType.DRAFT_DERIVED)
energy_deficit_surplus_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED)
energy_deficit_surplus_metadata.set_evidence("known_limitations", EvidenceType.MIXED) # Thresholds from code, interpretation mixed
energy_deficit_surplus_metadata.set_evidence("layer_1_decision", EvidenceType.TO_VERIFY)
energy_deficit_surplus_metadata.set_evidence("layer_2a_decision", EvidenceType.TO_VERIFY)
energy_deficit_surplus_metadata.set_evidence("layer_2b_reuse_possible", EvidenceType.TO_VERIFY)
register_placeholder(energy_deficit_surplus_metadata)
# ═══════════════════════════════════════════════════════════════════════════════
# 4. intake_volatility
# ═══════════════════════════════════════════════════════════════════════════════
intake_volatility_metadata = PlaceholderMetadata(
key="intake_volatility",
category="Ernährung",
description="Intake-Volatilität (Klassifikation)",
# Technical
resolver_module="backend/placeholder_resolver.py",
resolver_function="_safe_str('intake_volatility', pid)",
data_layer_module="backend/data_layer/nutrition_metrics.py",
data_layer_function="calculate_intake_volatility",
source_tables=["nutrition_log"],
# Semantic
semantic_contract="Liefert qualitative Klassifikation der Variabilität der täglichen Kalorienaufnahme über 28 Tage. 'stable' = hohe Konstanz, 'moderate' = mittlere Schwankung, 'high' = starke Variabilität.",
business_meaning="Konsistenz- und Adhärenzindikator für Ernährungsumsetzung. Zeigt, wie gleichmäßig die Kalorienaufnahme über die Zeit ist.",
unit="category (string)",
time_window="28d",
output_type=OutputType.STRING,
placeholder_type=PlaceholderType.INTERPRETED,
format_hint="Einer von drei Werten: 'stable', 'moderate', 'high'",
example_output="moderate",
# Quality
minimum_data_requirements="Wie macro_consistency_score: mindestens 18 Einträge in 28 Tagen (60% coverage).",
quality_filter_policy="Ausreißer, lückenhafte Tage und unvollständige Logs reduzieren Verlässlichkeit. Abgeleitet von macro_consistency_score.",
confidence_logic="Aus Datenabdeckung und Vollständigkeit ableiten. Abhängig von macro_consistency_score Confidence.",
missing_value_policy=MissingValuePolicy(
available=False,
value_raw=None,
missing_reason="insufficient_data",
legacy_display="nicht verfügbar"
),
known_limitations=(
"Klassifikation basiert auf macro_consistency_score: "
"score >= 80: 'stable', score >= 60: 'moderate', score < 60: 'high'. "
"WICHTIG: Hohe Volatilität ist nicht automatisch schlecht (bewusste Refeed-/Diet-Break-Tage, unregelmäßige Wochenenden). "
"Interpretation hängt von Zielkontext und Trainingslogik ab. "
"Vereinfacht komplexes Konsistenzmuster zu drei Kategorien."
),
# Architecture
layer_1_decision="Data Layer (nutrition_metrics.calculate_intake_volatility) - mapped macro_consistency_score zu Kategorie",
layer_2a_decision="Placeholder Resolver (_safe_str, keine zusätzliche Logik)",
layer_2b_reuse_possible="Ja - Kategorie-Anzeige oder Trend-Chart",
architecture_alignment="Phase 0c conform - Kategorisierung in Data Layer",
issue_53_alignment="Konform - Mapping in Data Layer",
# Evidence
evidence={}
)
# Evidence tagging
intake_volatility_metadata.set_evidence("resolver_module", EvidenceType.CODE_DERIVED)
intake_volatility_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED)
intake_volatility_metadata.set_evidence("data_layer_module", EvidenceType.CODE_DERIVED)
intake_volatility_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED)
intake_volatility_metadata.set_evidence("source_tables", EvidenceType.CODE_DERIVED)
intake_volatility_metadata.set_evidence("unit", EvidenceType.CODE_DERIVED)
intake_volatility_metadata.set_evidence("time_window", EvidenceType.CODE_DERIVED)
intake_volatility_metadata.set_evidence("output_type", EvidenceType.CODE_DERIVED)
intake_volatility_metadata.set_evidence("format_hint", EvidenceType.CODE_DERIVED)
intake_volatility_metadata.set_evidence("minimum_data_requirements", EvidenceType.MIXED)
intake_volatility_metadata.set_evidence("quality_filter_policy", EvidenceType.MIXED)
intake_volatility_metadata.set_evidence("confidence_logic", EvidenceType.MIXED)
intake_volatility_metadata.set_evidence("semantic_contract", EvidenceType.DRAFT_DERIVED)
intake_volatility_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED)
intake_volatility_metadata.set_evidence("known_limitations", EvidenceType.MIXED) # Thresholds from code, interpretation mixed
intake_volatility_metadata.set_evidence("layer_1_decision", EvidenceType.TO_VERIFY)
intake_volatility_metadata.set_evidence("layer_2a_decision", EvidenceType.TO_VERIFY)
intake_volatility_metadata.set_evidence("layer_2b_reuse_possible", EvidenceType.TO_VERIFY)
register_placeholder(intake_volatility_metadata)
# ═══════════════════════════════════════════════════════════════════════════════
# 5. nutrition_days
# ═══════════════════════════════════════════════════════════════════════════════
nutrition_days_metadata = PlaceholderMetadata(
key="nutrition_days",
category="Ernährung",
description="Anzahl valider Ernährungstage (30d)",
# Technical
resolver_module="backend/placeholder_resolver.py",
resolver_function="get_nutrition_days(pid, 30)",
data_layer_module="backend/data_layer/nutrition_metrics.py",
data_layer_function="get_nutrition_days_data",
source_tables=["nutrition_log"],
# Semantic
semantic_contract="Liefert die Anzahl der Tage mit valider Ernährungserfassung im 30-Tage-Fenster. Zählt alle unique Datums-Einträge in nutrition_log.",
business_meaning="Direktes Maß für Datenabdeckung und Aussagekraft der Ernährungsplaceholder. Zeigt, an wie vielen Tagen im Zeitfenster Ernährungsdaten erfasst wurden.",
unit="days",
time_window="30d",
output_type=OutputType.NUMERIC,
placeholder_type=PlaceholderType.META,
format_hint="Ganzzahl 0-30",
example_output="22",
# Quality
minimum_data_requirements="Keine Mindestmenge für Existenz des Placeholders selbst. Wert kann 0 sein.",
quality_filter_policy=(
"Definition 'valider Tag': Jeder Tag mit mindestens einem Eintrag in nutrition_log gilt als valide. "
"WICHTIG: Sagt NICHTS über Qualität oder Vollständigkeit des einzelnen Tages. "
"Auch Teil-Tage (z.B. nur Frühstück erfasst) zählen als valider Tag. "
"Keine Prüfung auf Mindest-Kalorienanzahl oder vollständige Makros."
),
confidence_logic="Nicht klassisch nötig - der Wert selbst dient als Verlässlichkeitsindikator für andere Ernährungsplaceholder.",
missing_value_policy=MissingValuePolicy(
available=True, # Always available, even if 0 days
value_raw=0,
missing_reason=None,
legacy_display="0"
),
known_limitations=(
"Zählt nur UNIQUE dates mit Einträgen, nicht die Anzahl der Einträge. "
"Sagt nichts über Qualität der einzelnen Tage (z.B. Vollständigkeit, Plausibilität). "
"Nur Abdeckungsmaß, kein Qualitätsmaß. "
"Bei mehreren Einträgen pro Tag wird Tag nur einmal gezählt."
),
# Architecture
layer_1_decision="Data Layer (nutrition_metrics.get_nutrition_days_data) - zählt unique dates",
layer_2a_decision="Placeholder Resolver (get_nutrition_days, formatiert zu String)",
layer_2b_reuse_possible="Ja - Coverage-Chart oder Datenqualitäts-Dashboard",
architecture_alignment="Phase 0c conform - Count in Data Layer, Formatting in Resolver",
issue_53_alignment="Konform - Zählung in Data Layer",
# Evidence
evidence={}
)
# Evidence tagging
nutrition_days_metadata.set_evidence("resolver_module", EvidenceType.CODE_DERIVED)
nutrition_days_metadata.set_evidence("resolver_function", EvidenceType.CODE_DERIVED)
nutrition_days_metadata.set_evidence("data_layer_module", EvidenceType.CODE_DERIVED)
nutrition_days_metadata.set_evidence("data_layer_function", EvidenceType.CODE_DERIVED)
nutrition_days_metadata.set_evidence("source_tables", EvidenceType.CODE_DERIVED)
nutrition_days_metadata.set_evidence("unit", EvidenceType.CODE_DERIVED)
nutrition_days_metadata.set_evidence("time_window", EvidenceType.CODE_DERIVED)
nutrition_days_metadata.set_evidence("output_type", EvidenceType.CODE_DERIVED)
nutrition_days_metadata.set_evidence("format_hint", EvidenceType.CODE_DERIVED)
nutrition_days_metadata.set_evidence("minimum_data_requirements", EvidenceType.CODE_DERIVED)
nutrition_days_metadata.set_evidence("quality_filter_policy", EvidenceType.MIXED) # Logic from code, definition from inspection
nutrition_days_metadata.set_evidence("confidence_logic", EvidenceType.DRAFT_DERIVED)
nutrition_days_metadata.set_evidence("semantic_contract", EvidenceType.MIXED) # Count logic from code, interpretation from draft
nutrition_days_metadata.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED)
nutrition_days_metadata.set_evidence("known_limitations", EvidenceType.MIXED)
nutrition_days_metadata.set_evidence("layer_1_decision", EvidenceType.TO_VERIFY)
nutrition_days_metadata.set_evidence("layer_2a_decision", EvidenceType.TO_VERIFY)
nutrition_days_metadata.set_evidence("layer_2b_reuse_possible", EvidenceType.TO_VERIFY)
register_placeholder(nutrition_days_metadata)
# ═══════════════════════════════════════════════════════════════════════════════
# Registration Summary
# ═══════════════════════════════════════════════════════════════════════════════
"""
Part C Registration Complete:
- macro_consistency_score: Score-based consistency indicator (CV-based)
- energy_balance_7d: kcal/day average balance (intake - estimated TDEE)
- energy_deficit_surplus: Status classification (deficit/maintenance/surplus)
- intake_volatility: Category classification (stable/moderate/high)
- nutrition_days: Count of valid nutrition days (meta indicator)
Total Nutrition Cluster:
- Part A: 4 placeholders (kcal_avg, protein_avg, carb_avg, fat_avg)
- Part B: 5 placeholders (protein targets + adequacy)
- Part C: 5 placeholders (consistency + balance + meta)
14 nutrition placeholders total
All registrations follow Phase 0c Multi-Layer Architecture:
- Layer 1 (Data Layer): Calculations
- Layer 2a (Placeholder Resolver): Formatting only
- No logic changes from existing implementations
- Evidence-based metadata tagging
"""

View File

@ -0,0 +1,281 @@
"""
Placeholder/Metric Registry Framework
Central registry for all placeholders/metrics ensuring consistent metadata across:
- Backend prompt resolution (Layer 2a)
- GUI selection lists
- Extended export
- Validation
- Chart assignment (Layer 2b)
Version: 1.0 (Part A - Nutrition Basis Metrics)
"""
from dataclasses import dataclass, field, asdict
from typing import Callable, Dict, List, Optional, Any
from enum import Enum
class EvidenceType(str, Enum):
"""Evidence type for metadata fields."""
CODE_DERIVED = "code-derived"
DRAFT_DERIVED = "draft-derived"
MIXED = "mixed"
UNRESOLVED = "unresolved"
TO_VERIFY = "to_verify"
class OutputType(str, Enum):
"""Placeholder output types."""
NUMERIC = "numeric"
STRING = "string"
BOOLEAN = "boolean"
JSON = "json"
LIST = "list"
TEXT_SUMMARY = "text_summary"
class PlaceholderType(str, Enum):
"""Placeholder semantic types."""
ATOMIC = "atomic"
RAW_DATA = "raw_data"
INTERPRETED = "interpreted"
SCORE = "score"
META = "meta"
@dataclass
class MissingValuePolicy:
"""Structured missing value handling."""
available: bool
value_raw: Optional[Any]
missing_reason: str # no_data, insufficient_data, resolver_error, calculation_error, not_applicable
legacy_display: str
@dataclass
class PlaceholderMetadata:
"""
Complete metadata for a placeholder/metric.
All fields track their evidence type to maintain transparency
about what is code-derived vs. draft-derived.
"""
# Core identification
key: str
category: str
description: str
# Technical (typically code-derived)
resolver_module: str
resolver_function: str
data_layer_module: Optional[str] = None
data_layer_function: Optional[str] = None
source_tables: List[str] = field(default_factory=list)
# Semantic (typically draft-derived or mixed)
semantic_contract: str = ""
business_meaning: str = ""
unit: str = ""
time_window: str = ""
output_type: OutputType = OutputType.STRING
placeholder_type: PlaceholderType = PlaceholderType.INTERPRETED
format_hint: str = ""
example_output: str = ""
# Quality (mixed sources)
minimum_data_requirements: Optional[str] = None
quality_filter_policy: Optional[str] = None
confidence_logic: Optional[str] = None
missing_value_policy: Optional[MissingValuePolicy] = None
known_limitations: Optional[str] = None
# Architecture (code-derived)
layer_1_decision: Optional[str] = None
layer_2a_decision: Optional[str] = None
layer_2b_reuse_possible: Optional[bool] = None
architecture_alignment: Optional[str] = None
issue_53_alignment: Optional[str] = None
# Evidence tracking
evidence: Dict[str, EvidenceType] = field(default_factory=dict)
# Runtime resolver (not serialized to export)
_resolver_func: Optional[Callable] = field(default=None, repr=False, compare=False)
def to_dict(self, include_resolver: bool = False) -> Dict:
"""Convert to dictionary for export."""
data = asdict(self)
# Remove private fields
if not include_resolver:
data.pop('_resolver_func', None)
# Convert enums to strings
data['output_type'] = self.output_type.value
data['placeholder_type'] = self.placeholder_type.value
# Convert evidence dict
data['evidence'] = {k: v.value for k, v in self.evidence.items()}
# Convert missing_value_policy
if self.missing_value_policy:
data['missing_value_policy'] = asdict(self.missing_value_policy)
return data
def get_evidence(self, field_name: str) -> Optional[EvidenceType]:
"""Get evidence type for a field."""
return self.evidence.get(field_name)
def set_evidence(self, field_name: str, evidence_type: EvidenceType):
"""Set evidence type for a field."""
self.evidence[field_name] = evidence_type
def validate(self) -> List[str]:
"""Validate metadata completeness."""
issues = []
if not self.key:
issues.append("Missing key")
if not self.category:
issues.append("Missing category")
if not self.description:
issues.append("Missing description")
if not self.resolver_module:
issues.append("Missing resolver_module")
if not self.resolver_function:
issues.append("Missing resolver_function")
if not self.semantic_contract:
issues.append("Missing semantic_contract")
if not self.unit:
issues.append("Missing unit")
if not self.time_window:
issues.append("Missing time_window")
return issues
class PlaceholderRegistry:
"""
Central registry for all placeholders/metrics.
Ensures single source of truth for metadata across all consumers.
"""
def __init__(self):
self._registry: Dict[str, PlaceholderMetadata] = {}
def register(
self,
metadata: PlaceholderMetadata,
resolver_func: Optional[Callable] = None
):
"""
Register a placeholder with complete metadata.
Args:
metadata: Complete placeholder metadata
resolver_func: Optional resolver function (for runtime resolution)
"""
if metadata.key in self._registry:
raise ValueError(f"Placeholder {metadata.key} already registered")
if resolver_func:
metadata._resolver_func = resolver_func
self._registry[metadata.key] = metadata
def get(self, key: str) -> Optional[PlaceholderMetadata]:
"""Get metadata for a placeholder."""
return self._registry.get(key)
def get_all(self) -> Dict[str, PlaceholderMetadata]:
"""Get all registered placeholders."""
return self._registry.copy()
def get_by_category(self, category: str) -> List[PlaceholderMetadata]:
"""Get placeholders by category (for GUI selection lists)."""
return [
m for m in self._registry.values()
if m.category == category
]
def get_all_for_export(self) -> List[Dict]:
"""Get all metadata for extended export."""
return [m.to_dict() for m in self._registry.values()]
def get_by_evidence_type(self, evidence_type: EvidenceType) -> Dict[str, List[str]]:
"""
Get fields by evidence type (for quality assurance).
Returns:
Dict mapping placeholder_key to list of field_names with that evidence type
"""
result = {}
for key, metadata in self._registry.items():
fields = [
field_name
for field_name, ev_type in metadata.evidence.items()
if ev_type == evidence_type
]
if fields:
result[key] = fields
return result
def validate_all(self) -> Dict[str, List[str]]:
"""
Validate all registered placeholders.
Returns:
Dict mapping placeholder_key to list of validation issues
"""
issues = {}
for key, metadata in self._registry.items():
validation_issues = metadata.validate()
if validation_issues:
issues[key] = validation_issues
return issues
def resolve(self, key: str, profile_id: str) -> str:
"""
Resolve a placeholder value for a profile.
Args:
key: Placeholder key
profile_id: User profile ID
Returns:
Resolved value as string
"""
metadata = self.get(key)
if not metadata:
raise ValueError(f"Placeholder {key} not registered")
if not metadata._resolver_func:
raise ValueError(f"Placeholder {key} has no resolver function")
return metadata._resolver_func(profile_id)
# Global registry instance
_global_registry = PlaceholderRegistry()
def get_registry() -> PlaceholderRegistry:
"""Get the global placeholder registry."""
return _global_registry
def register_placeholder(
metadata: PlaceholderMetadata,
resolver_func: Optional[Callable] = None
):
"""
Register a placeholder in the global registry.
Args:
metadata: Complete placeholder metadata
resolver_func: Optional resolver function
"""
_global_registry.register(metadata, resolver_func)

View File

@ -0,0 +1,136 @@
"""
Placeholder Registry Export Integration
Integrates the new placeholder registry with the existing export system.
Provides backward-compatible export with enhanced metadata from registry.
"""
from typing import Dict, List
from placeholder_registry import get_registry, EvidenceType
def get_registry_metadata_for_export(profile_id: str) -> Dict:
"""
Get metadata from registry formatted for export.
Returns:
Dict with:
- flat: List of all metadata dicts
- by_category: Dict mapping category to list of metadata
- evidence_report: Statistics about evidence types
- validation_report: Validation issues
"""
registry = get_registry()
# Get all metadata
all_metadata = registry.get_all()
# Build flat export
flat = []
for key, metadata in sorted(all_metadata.items()):
meta_dict = metadata.to_dict()
flat.append(meta_dict)
# Build by_category
by_category = {}
for metadata in all_metadata.values():
cat = metadata.category
if cat not in by_category:
by_category[cat] = []
by_category[cat].append(metadata.to_dict())
# Evidence report
evidence_stats = {
"code_derived": len(registry.get_by_evidence_type(EvidenceType.CODE_DERIVED)),
"draft_derived": len(registry.get_by_evidence_type(EvidenceType.DRAFT_DERIVED)),
"mixed": len(registry.get_by_evidence_type(EvidenceType.MIXED)),
"unresolved": len(registry.get_by_evidence_type(EvidenceType.UNRESOLVED)),
"to_verify": len(registry.get_by_evidence_type(EvidenceType.TO_VERIFY))
}
evidence_detail = {
etype.value: registry.get_by_evidence_type(etype)
for etype in EvidenceType
}
# Validation report
validation_issues = registry.validate_all()
return {
"flat": flat,
"by_category": by_category,
"evidence_report": {
"statistics": evidence_stats,
"detail": evidence_detail
},
"validation_report": validation_issues
}
def merge_registry_with_legacy_export(
registry_data: Dict,
legacy_data: Dict,
resolved_values: Dict[str, str]
) -> Dict:
"""
Merge registry metadata with legacy export data.
Args:
registry_data: Data from get_registry_metadata_for_export()
legacy_data: Existing legacy export structure
resolved_values: Resolved placeholder values (key -> value)
Returns:
Merged export with registry enhancements
"""
# Start with legacy structure
merged = legacy_data.copy()
# Add registry metadata section
merged["registry_metadata"] = {
"flat": registry_data["flat"],
"by_category": registry_data["by_category"],
"evidence_report": registry_data["evidence_report"],
"validation_report": registry_data["validation_report"]
}
# Populate runtime values in registry metadata
for meta_dict in merged["registry_metadata"]["flat"]:
key = meta_dict["key"]
if key in resolved_values:
meta_dict["value_display"] = resolved_values[key]
# Note: value_raw extraction can be added here if needed
return merged
def get_enhanced_export_with_registry(profile_id: str, legacy_export: Dict) -> Dict:
"""
Enhance legacy export with registry metadata.
Args:
profile_id: User profile ID
legacy_export: Existing legacy export structure
Returns:
Enhanced export with registry metadata section
"""
# Get registry data
registry_data = get_registry_metadata_for_export(profile_id)
# Get resolved values (for value_display population)
from placeholder_resolver import get_placeholder_example_values
resolved_values = get_placeholder_example_values(profile_id)
cleaned_values = {
key.replace('{{', '').replace('}}', ''): value
for key, value in resolved_values.items()
}
# Merge
enhanced = merge_registry_with_legacy_export(
registry_data,
legacy_export,
cleaned_values
)
return enhanced

View File

@ -358,13 +358,28 @@ def export_placeholder_values_extended(
metadata.missing_reason = "Placeholder not in resolver output"
# Generate gap report (collect unresolved fields)
# Note: TimeWindow, OutputType, PlaceholderType are from old metadata system
# Skip gap report for old metadata if not available
gaps = {}
try:
from placeholder_metadata_complete import TimeWindow, OutputType, PlaceholderType
gaps = {
'unknown_time_window': [k for k, m in all_metadata.items() if m.time_window == TimeWindow.UNKNOWN],
'unknown_output_type': [k for k, m in all_metadata.items() if m.output_type == OutputType.UNKNOWN],
'legacy_unknown_type': [k for k, m in all_metadata.items() if m.type == PlaceholderType.LEGACY_UNKNOWN],
'unresolved_fields': {k: m.unresolved_fields for k, m in all_metadata.items() if m.unresolved_fields},
'legacy_mismatches': [k for k, m in all_metadata.items() if m.legacy_contract_mismatch],
'orphaned': [k for k, m in all_metadata.items() if m.orphaned_placeholder],
'unknown_time_window': [k for k, m in all_metadata.items() if hasattr(m, 'time_window') and m.time_window == TimeWindow.UNKNOWN],
'unknown_output_type': [k for k, m in all_metadata.items() if hasattr(m, 'output_type') and m.output_type == OutputType.UNKNOWN],
'legacy_unknown_type': [k for k, m in all_metadata.items() if hasattr(m, 'type') and m.type == PlaceholderType.LEGACY_UNKNOWN],
'unresolved_fields': {k: m.unresolved_fields for k, m in all_metadata.items() if hasattr(m, 'unresolved_fields') and m.unresolved_fields},
'legacy_mismatches': [k for k, m in all_metadata.items() if hasattr(m, 'legacy_contract_mismatch') and m.legacy_contract_mismatch],
'orphaned': [k for k, m in all_metadata.items() if hasattr(m, 'orphaned_placeholder') and m.orphaned_placeholder],
}
except ImportError:
# Old metadata system not available, use empty gaps
gaps = {
'unknown_time_window': [],
'unknown_output_type': [],
'legacy_unknown_type': [],
'unresolved_fields': {},
'legacy_mismatches': [],
'orphaned': [],
}
# Validation
@ -466,6 +481,24 @@ def export_placeholder_values_extended(
}
}
# ── PART A: Registry Integration ─────────────────────────────────────────
# Add registry metadata for Part A placeholders (kcal_avg, protein_avg, carb_avg, fat_avg)
try:
import placeholder_registrations # Auto-registers Part A placeholders
from placeholder_registry_export import get_registry_metadata_for_export
registry_data = get_registry_metadata_for_export(profile_id)
export_data['registry_metadata'] = registry_data
except Exception as e:
# Graceful degradation if registry not available
export_data['registry_metadata'] = {
"error": f"Registry not available: {str(e)}",
"flat": [],
"by_category": {},
"evidence_report": {},
"validation_report": {}
}
# Fill validation
for key, violations in validation_results.items():
errors = [v for v in violations if v.severity == "error"]

63
create_issue_no_jq.sh Normal file
View File

@ -0,0 +1,63 @@
#!/bin/bash
#
# Script zum Anlegen des Gitea Issues ohne jq dependency
#
GITEA_TOKEN="b3d27c7d87d2acf39490d0c58f26922164edb4e8"
GITEA_URL="http://192.168.2.144:3000"
REPO_OWNER="Lars"
REPO_NAME="mitai-jinkendo"
# Issue Body aus Datei lesen (erste 29 Zeilen überspringen = Metadaten)
ISSUE_BODY=$(tail -n +30 "c:/Dev/mitai-jinkendo/.claude/task/rework_0b_placeholder/ISSUE_METADATEN_REVIEW.md" | python3 -c "import sys, json; print(json.dumps(sys.stdin.read()))")
# JSON Payload mit Python erstellen
python3 << PYEOF > /tmp/issue_payload.json
import json
body = $ISSUE_BODY
payload = {
"title": "Placeholder Registry: UNRESOLVED & TO_VERIFY Metadaten prüfen",
"body": body,
"labels": [2, 3]
}
print(json.dumps(payload, ensure_ascii=False))
PYEOF
echo "Erstelle Gitea Issue..."
echo "Repository: $REPO_OWNER/$REPO_NAME"
echo ""
# Issue via API anlegen
RESPONSE=$(curl -s -X POST \
"$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/issues" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d @/tmp/issue_payload.json)
# Response mit Python parsen
python3 << PYEOF
import json
response = '''$RESPONSE'''
try:
data = json.loads(response)
if 'number' in data:
print(f"✓ Issue erfolgreich erstellt!")
print(f"")
print(f"Issue #{data['number']}")
print(f"URL: {data['html_url']}")
print(f"")
print(f"✓ Fertig!")
else:
print(f"✗ Fehler beim Erstellen des Issues:")
print(json.dumps(data, indent=2))
except Exception as e:
print(f"✗ Fehler: {e}")
print(response)
PYEOF
# Cleanup
rm -f /tmp/issue_payload.json

View File

@ -0,0 +1,78 @@
#!/bin/bash
#
# Script zum Anlegen des Gitea Issues:
# "Placeholder Registry: UNRESOLVED & TO_VERIFY Metadaten prüfen"
#
# Usage: ./create_metadaten_review_issue.sh YOUR_GITEA_TOKEN
#
if [ -z "$1" ]; then
echo "ERROR: Gitea Token erforderlich"
echo "Usage: $0 YOUR_GITEA_TOKEN"
echo ""
echo "Token erstellen:"
echo " 1. Gitea öffnen: http://192.168.2.144:3000"
echo " 2. Settings → Applications → Generate New Token"
echo " 3. Name: 'Claude Code Issue Management'"
echo " 4. Scope: issue (read/write)"
echo " 5. Token kopieren und als Argument übergeben"
exit 1
fi
GITEA_TOKEN="$1"
GITEA_URL="http://192.168.2.144:3000"
REPO_OWNER="Lars"
REPO_NAME="mitai-jinkendo"
# Issue Body aus Datei lesen (erste 30 Zeilen überspringen = Metadaten)
ISSUE_BODY=$(tail -n +30 .claude/task/rework_0b_placeholder/ISSUE_METADATEN_REVIEW.md)
# JSON Payload erstellen
cat > /tmp/gitea_issue_payload.json << EOF
{
"title": "Placeholder Registry: UNRESOLVED & TO_VERIFY Metadaten prüfen",
"body": $(echo "$ISSUE_BODY" | jq -Rs .),
"labels": [1, 2, 3],
"priority": 2
}
EOF
echo "Erstelle Gitea Issue..."
echo "Repository: $REPO_OWNER/$REPO_NAME"
echo ""
# Issue via API anlegen
RESPONSE=$(curl -s -X POST \
"$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/issues" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d @/tmp/gitea_issue_payload.json)
# Response prüfen
if echo "$RESPONSE" | grep -q '"number"'; then
ISSUE_NUMBER=$(echo "$RESPONSE" | jq -r '.number')
ISSUE_URL=$(echo "$RESPONSE" | jq -r '.html_url')
echo "✓ Issue erfolgreich erstellt!"
echo ""
echo "Issue #$ISSUE_NUMBER"
echo "URL: $ISSUE_URL"
echo ""
# Labels setzen (falls nicht automatisch gesetzt)
echo "Setze Labels..."
curl -s -X POST \
"$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/issues/$ISSUE_NUMBER/labels" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"labels": [1, 2, 3]}' > /dev/null
echo "✓ Fertig!"
else
echo "✗ Fehler beim Erstellen des Issues:"
echo "$RESPONSE" | jq .
exit 1
fi
# Cleanup
rm -f /tmp/gitea_issue_payload.json

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

75
package-lock.json generated Normal file
View File

@ -0,0 +1,75 @@
{
"name": "mitai-jinkendo",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"devDependencies": {
"@playwright/test": "^1.58.2"
}
},
"node_modules/@playwright/test": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.58.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.58.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
}
}
}

5
package.json Normal file
View File

@ -0,0 +1,5 @@
{
"devDependencies": {
"@playwright/test": "^1.58.2"
}
}

12
playwright.config.js Normal file
View File

@ -0,0 +1,12 @@
module.exports = {
testDir: './tests',
timeout: 30000,
use: {
channel: 'chrome',
headless: true,
viewport: { width: 390, height: 844 },
screenshot: 'only-on-failure',
baseURL: 'https://dev.mitai.jinkendo.de',
},
reporter: 'list',
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,6 @@
{
"status": "failed",
"failedTests": [
"d6ae548bbe32e0652471-816c0db33a38f27f1eaf"
]
}

View File

@ -0,0 +1,202 @@
"""
Test script to verify Activity Cluster placeholder registration.
Verifies:
1. All 17 Activity placeholders are registered
2. All have complete metadata (22 mandatory fields)
3. Evidence distribution is correct
"""
import sys
from pathlib import Path
# Add backend to path
sys.path.insert(0, str(Path(__file__).parent / 'backend'))
# Import registrations (triggers auto-registration)
print("Importing placeholder_registry...")
from placeholder_registry import EvidenceType, get_registry
print("Importing activity_metrics...")
try:
from placeholder_registrations import activity_metrics
print("Activity metrics imported successfully")
except Exception as e:
print(f"ERROR importing activity_metrics: {e}")
import traceback
traceback.print_exc()
METADATA_REGISTRY = get_registry()
print(f"Registry size after import: {len(METADATA_REGISTRY.get_all())}")
# Expected placeholders
EXPECTED_ACTIVITY_PLACEHOLDERS = [
'activity_summary',
'activity_detail',
'trainingstyp_verteilung',
'training_minutes_week',
'training_frequency_7d',
'quality_sessions_pct',
'ability_balance_strength',
'ability_balance_endurance',
'ability_balance_mental',
'ability_balance_coordination',
'ability_balance_mobility',
'proxy_internal_load_7d',
'monotony_score',
'strain_score',
'rest_day_compliance',
'vo2max_trend_28d',
'activity_score',
]
def test_registration():
"""Test that all Activity placeholders are registered."""
print("=== Activity Cluster Registration Test ===\n")
# Check all expected placeholders
registered = []
missing = []
for key in EXPECTED_ACTIVITY_PLACEHOLDERS:
if METADATA_REGISTRY.get(key) is not None:
registered.append(key)
else:
missing.append(key)
print(f"OK Registered: {len(registered)}/17")
if missing:
print(f"FAIL Missing: {len(missing)}/17")
for key in missing:
print(f" - {key}")
return False
print(f"OK All 17 Activity placeholders registered\n")
return True
def test_metadata_completeness():
"""Test that all registered placeholders have complete metadata."""
print("=== Metadata Completeness Test ===\n")
mandatory_fields = [
'key', 'category', 'name_de', 'name_en', 'description_de', 'description_en',
'placeholder_type', 'output_type', 'unit', 'time_window', 'semantic_contract',
'calculation_method', 'source_info', 'data_lineage', 'confidence_logic',
'missing_value_policy', 'known_limitations', 'dependencies',
'layer_2b_reuse_possible', 'example_value'
]
incomplete = []
for key in EXPECTED_ACTIVITY_PLACEHOLDERS:
metadata = METADATA_REGISTRY.get(key)
if metadata is None:
continue
missing_fields = []
for field in mandatory_fields:
value = getattr(metadata, field, None)
if value is None or value == '' or value == []:
missing_fields.append(field)
if missing_fields:
incomplete.append((key, missing_fields))
if incomplete:
print(f"FAIL Incomplete metadata: {len(incomplete)}/17")
for key, fields in incomplete:
print(f" - {key}: missing {fields}")
return False
print(f"OK All 17 placeholders have complete metadata (20 mandatory fields)\n")
return True
def test_evidence_distribution():
"""Test evidence tagging distribution."""
print("=== Evidence Distribution Test ===\n")
evidence_counts = {
EvidenceType.CODE_DERIVED: 0,
EvidenceType.DRAFT_DERIVED: 0,
EvidenceType.MIXED: 0,
EvidenceType.TO_VERIFY: 0,
EvidenceType.UNRESOLVED: 0,
}
total_tags = 0
for key in EXPECTED_ACTIVITY_PLACEHOLDERS:
metadata = METADATA_REGISTRY.get(key)
if metadata is None:
continue
# Count evidence tags (22 fields)
for field in metadata.get_all_evidence_fields():
evidence = metadata.get_evidence(field)
if evidence:
evidence_counts[evidence] = evidence_counts.get(evidence, 0) + 1
total_tags += 1
print(f"Total evidence tags: {total_tags} (expected ~374 = 17 × 22)")
print("\nDistribution:")
for evidence_type, count in evidence_counts.items():
percentage = (count / total_tags * 100) if total_tags > 0 else 0
print(f" {evidence_type.value:15s}: {count:3d} ({percentage:5.1f}%)")
print("\nExpected distribution:")
print(" CODE_DERIVED: ~60% (directly from code)")
print(" DRAFT_DERIVED: ~15% (from canonical draft)")
print(" MIXED: ~15% (combined sources)")
print(" TO_VERIFY: ~10% (needs verification)")
print()
return True
def dump_sample_placeholder():
"""Dump one complete placeholder as sample."""
print("=== Sample Placeholder: activity_score ===\n")
metadata = METADATA_REGISTRY.get('activity_score')
if metadata is None:
print("FAIL activity_score not found in registry")
return False
print(f"Key: {metadata.key}")
print(f"Category: {metadata.category}")
print(f"Name (DE): {metadata.name_de}")
print(f"Name (EN): {metadata.name_en}")
print(f"Type: {metadata.placeholder_type.value}")
print(f"Output: {metadata.output_type.value}")
print(f"Unit: {metadata.unit}")
print(f"Time Window: {metadata.time_window}")
print(f"\nDescription (DE):")
print(f" {metadata.description_de[:100]}...")
print(f"\nSemantic Contract:")
print(f" {metadata.semantic_contract[:100]}...")
print(f"\nCalculation Method:")
print(f" {metadata.calculation_method[:100]}...")
print(f"\nKnown Limitations:")
print(f" {metadata.known_limitations[:150]}...")
print(f"\nDependencies: {len(metadata.dependencies)} items")
print(f"Layer 2b Reuse: {metadata.layer_2b_reuse_possible}")
print()
return True
if __name__ == '__main__':
success = True
success &= test_registration()
success &= test_metadata_completeness()
success &= test_evidence_distribution()
success &= dump_sample_placeholder()
if success:
print("OK All tests passed - Activity Cluster registration is complete and valid")
sys.exit(0)
else:
print("FAIL Some tests failed - see output above")
sys.exit(1)

View File

@ -0,0 +1,65 @@
const { test, expect } = require('@playwright/test');
const TEST_EMAIL = process.env.TEST_EMAIL || 'lars@stommer.com';
const TEST_PASSWORD = process.env.TEST_PASSWORD || '5112';
async function login(page) {
await page.goto('/');
await page.waitForLoadState('networkidle');
await page.fill('input[type="email"]', TEST_EMAIL);
await page.fill('input[type="password"]', TEST_PASSWORD);
await page.click('button:has-text("Anmelden")');
await page.waitForLoadState('networkidle');
}
test('1. Login funktioniert', async ({ page }) => {
await page.goto('/');
await page.fill('input[type="email"]', TEST_EMAIL);
await page.fill('input[type="password"]', TEST_PASSWORD);
await page.click('button:has-text("Anmelden")');
await page.waitForLoadState('networkidle');
const loginButton = page.locator('button:has-text("Anmelden")');
await expect(loginButton).toHaveCount(0, { timeout: 10000 });
await page.screenshot({ path: 'screenshots/01-nach-login.png' });
console.log('Login erfolgreich');
});
test('2. Dashboard laedt ohne Fehler', async ({ page }) => {
await login(page);
await expect(page.locator('.spinner')).toHaveCount(0, { timeout: 10000 });
await page.screenshot({ path: 'screenshots/02-dashboard.png' });
console.log('Dashboard OK');
});
test('3. Erfassung erreichbar', async ({ page }) => {
await login(page);
await page.click('text=Erfassung');
await page.waitForLoadState('networkidle');
await page.screenshot({ path: 'screenshots/03-erfassung.png' });
console.log('Erfassung OK');
});
test('4. Analyse erreichbar', async ({ page }) => {
await login(page);
await page.click('text=Analyse');
await page.waitForLoadState('networkidle');
await page.screenshot({ path: 'screenshots/04-analyse.png' });
console.log('Analyse OK');
});
test('5. Keine kritischen Console-Fehler', async ({ page }) => {
const errors = [];
page.on('console', msg => {
if (msg.type() === 'error') errors.push(msg.text());
});
await login(page);
await page.waitForLoadState('networkidle');
const kritisch = errors.filter(e =>
!e.includes('favicon') && !e.includes('sourceMap') && !e.includes('404')
);
if (kritisch.length > 0) {
console.log('Console-Fehler:', kritisch.join(', '));
} else {
console.log('Keine kritischen Console-Fehler');
}
});