develop #59
|
|
@ -27,6 +27,14 @@
|
||||||
- Production Deploy nur nach expliziter Freigabe
|
- Production Deploy nur nach expliziter Freigabe
|
||||||
- Migration 001-999 Pattern einhalten
|
- 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
|
## Projekt-Übersicht
|
||||||
**Mitai Jinkendo** (身体 Jinkendo) – selbst-gehostete PWA für Körper-Tracking mit KI-Auswertung.
|
**Mitai Jinkendo** (身体 Jinkendo) – selbst-gehostete PWA für Körper-Tracking mit KI-Auswertung.
|
||||||
Teil der **Jinkendo**-App-Familie (人拳道). Domains: jinkendo.de / .com / .life
|
Teil der **Jinkendo**-App-Familie (人拳道). Domains: jinkendo.de / .com / .life
|
||||||
|
|
|
||||||
|
|
@ -575,22 +575,23 @@ def calculate_protein_g_per_kg(profile_id: str) -> Optional[float]:
|
||||||
|
|
||||||
weight = float(weight_row['weight'])
|
weight = float(weight_row['weight'])
|
||||||
|
|
||||||
# Get protein intake
|
# Get protein intake aggregated by day (SUM per day)
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT protein_g
|
SELECT date, SUM(protein_g) as daily_protein
|
||||||
FROM nutrition_log
|
FROM nutrition_log
|
||||||
WHERE profile_id = %s
|
WHERE profile_id = %s
|
||||||
AND date >= CURRENT_DATE - INTERVAL '7 days'
|
AND date >= CURRENT_DATE - INTERVAL '7 days'
|
||||||
AND protein_g IS NOT NULL
|
AND protein_g IS NOT NULL
|
||||||
|
GROUP BY date
|
||||||
ORDER BY date DESC
|
ORDER BY date DESC
|
||||||
""", (profile_id,))
|
""", (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
|
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
|
protein_per_kg = avg_protein / weight
|
||||||
|
|
||||||
return round(protein_per_kg, 2)
|
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'])
|
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("""
|
cur.execute("""
|
||||||
SELECT protein_g, date
|
SELECT date, SUM(protein_g) as daily_protein
|
||||||
FROM nutrition_log
|
FROM nutrition_log
|
||||||
WHERE profile_id = %s
|
WHERE profile_id = %s
|
||||||
AND date >= CURRENT_DATE - INTERVAL '7 days'
|
AND date >= CURRENT_DATE - INTERVAL '7 days'
|
||||||
AND protein_g IS NOT NULL
|
AND protein_g IS NOT NULL
|
||||||
|
GROUP BY date
|
||||||
ORDER BY date DESC
|
ORDER BY date DESC
|
||||||
""", (profile_id,))
|
""", (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
|
return None
|
||||||
|
|
||||||
# Count days in target range
|
# Count days in target range
|
||||||
days_in_target = 0
|
days_in_target = 0
|
||||||
total_days = len(protein_data)
|
total_days = len(daily_data)
|
||||||
|
|
||||||
for row in protein_data:
|
for row in daily_data:
|
||||||
protein_per_kg = float(row['protein_g']) / weight
|
daily_protein = float(row['daily_protein'])
|
||||||
if target_low <= protein_per_kg <= target_high:
|
if target_low_g <= daily_protein <= target_high_g:
|
||||||
days_in_target += 1
|
days_in_target += 1
|
||||||
|
|
||||||
return f"{days_in_target}/{total_days}"
|
return f"{days_in_target}/{total_days}"
|
||||||
|
|
|
||||||
14
backend/placeholder_registrations/__init__.py
Normal file
14
backend/placeholder_registrations/__init__.py
Normal 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']
|
||||||
1112
backend/placeholder_registrations/activity_metrics.py
Normal file
1112
backend/placeholder_registrations/activity_metrics.py
Normal file
File diff suppressed because it is too large
Load Diff
1307
backend/placeholder_registrations/body_metrics.py
Normal file
1307
backend/placeholder_registrations/body_metrics.py
Normal file
File diff suppressed because it is too large
Load Diff
216
backend/placeholder_registrations/nutrition_part_a.py
Normal file
216
backend/placeholder_registrations/nutrition_part_a.py
Normal 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()
|
||||||
429
backend/placeholder_registrations/nutrition_part_b.py
Normal file
429
backend/placeholder_registrations/nutrition_part_b.py
Normal 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()
|
||||||
446
backend/placeholder_registrations/nutrition_part_c.py
Normal file
446
backend/placeholder_registrations/nutrition_part_c.py
Normal 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
|
||||||
|
"""
|
||||||
281
backend/placeholder_registry.py
Normal file
281
backend/placeholder_registry.py
Normal 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)
|
||||||
136
backend/placeholder_registry_export.py
Normal file
136
backend/placeholder_registry_export.py
Normal 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
|
||||||
|
|
@ -358,14 +358,29 @@ def export_placeholder_values_extended(
|
||||||
metadata.missing_reason = "Placeholder not in resolver output"
|
metadata.missing_reason = "Placeholder not in resolver output"
|
||||||
|
|
||||||
# Generate gap report (collect unresolved fields)
|
# Generate gap report (collect unresolved fields)
|
||||||
gaps = {
|
# Note: TimeWindow, OutputType, PlaceholderType are from old metadata system
|
||||||
'unknown_time_window': [k for k, m in all_metadata.items() if m.time_window == TimeWindow.UNKNOWN],
|
# Skip gap report for old metadata if not available
|
||||||
'unknown_output_type': [k for k, m in all_metadata.items() if m.output_type == OutputType.UNKNOWN],
|
gaps = {}
|
||||||
'legacy_unknown_type': [k for k, m in all_metadata.items() if m.type == PlaceholderType.LEGACY_UNKNOWN],
|
try:
|
||||||
'unresolved_fields': {k: m.unresolved_fields for k, m in all_metadata.items() if m.unresolved_fields},
|
from placeholder_metadata_complete import TimeWindow, OutputType, PlaceholderType
|
||||||
'legacy_mismatches': [k for k, m in all_metadata.items() if m.legacy_contract_mismatch],
|
gaps = {
|
||||||
'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
|
# Validation
|
||||||
validation_results = registry.validate_all()
|
validation_results = registry.validate_all()
|
||||||
|
|
@ -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
|
# Fill validation
|
||||||
for key, violations in validation_results.items():
|
for key, violations in validation_results.items():
|
||||||
errors = [v for v in violations if v.severity == "error"]
|
errors = [v for v in violations if v.severity == "error"]
|
||||||
|
|
|
||||||
63
create_issue_no_jq.sh
Normal file
63
create_issue_no_jq.sh
Normal 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
|
||||||
78
create_metadaten_review_issue.sh
Normal file
78
create_metadaten_review_issue.sh
Normal 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
|
||||||
1
nutrition_cluster_final.json
Normal file
1
nutrition_cluster_final.json
Normal file
File diff suppressed because one or more lines are too long
1
nutrition_cluster_final_check.json
Normal file
1
nutrition_cluster_final_check.json
Normal file
File diff suppressed because one or more lines are too long
75
package-lock.json
generated
Normal file
75
package-lock.json
generated
Normal 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
5
package.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.58.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
12
playwright.config.js
Normal file
12
playwright.config.js
Normal 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',
|
||||||
|
};
|
||||||
1
registry_export_final.json
Normal file
1
registry_export_final.json
Normal file
File diff suppressed because one or more lines are too long
1
registry_export_fixed.json
Normal file
1
registry_export_fixed.json
Normal file
File diff suppressed because one or more lines are too long
1
registry_export_new_data.json
Normal file
1
registry_export_new_data.json
Normal file
File diff suppressed because one or more lines are too long
1
registry_export_part_c.json
Normal file
1
registry_export_part_c.json
Normal file
File diff suppressed because one or more lines are too long
1
registry_export_part_c_fixed.json
Normal file
1
registry_export_part_c_fixed.json
Normal file
File diff suppressed because one or more lines are too long
1
registry_export_partb.json
Normal file
1
registry_export_partb.json
Normal file
File diff suppressed because one or more lines are too long
6
test-results/.last-run.json
Normal file
6
test-results/.last-run.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"status": "failed",
|
||||||
|
"failedTests": [
|
||||||
|
"d6ae548bbe32e0652471-816c0db33a38f27f1eaf"
|
||||||
|
]
|
||||||
|
}
|
||||||
202
test_activity_registration.py
Normal file
202
test_activity_registration.py
Normal 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)
|
||||||
65
tests/dev-smoke-test.spec.js
Normal file
65
tests/dev-smoke-test.spec.js
Normal 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');
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user