feat: Placeholder Registry Framework + Part A Nutrition Metrics
All checks were successful
Deploy Development / deploy (push) Successful in 51s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 15s

Part A Implementation (Nutrition Basis Metrics):
- Registry-based metadata system (flexible, not hardcoded)
- 4 placeholders registered: kcal_avg, protein_avg, carb_avg, fat_avg
- Evidence-based tagging (code-derived, draft-derived, unresolved, to_verify)
- Single source of truth for all consumers (Prompt, GUI, Export, Validation)

Technical:
- backend/placeholder_registry.py: Core registry framework
- backend/placeholder_registrations/nutrition_part_a.py: Part A registrations
- backend/placeholder_registry_export.py: Export integration
- backend/routers/prompts.py: /placeholders/export-values-extended integration

Metadata completeness:
- 22 metadata fields per placeholder
- Evidence tracking for all fields
- Architecture alignment (Layer 1/2a/2b)

NO LOGIC CHANGE:
- Data Layer unchanged (nutrition_metrics.py)
- Resolver unchanged (placeholder_resolver.py)
- Values identical (only metadata/export enhanced)

Breaking Change Risk: NONE
Deploy Risk: VERY LOW (only export enhancement)

Plan: .claude/task/rework_0b_placeholder/NUTRITION_PART_A_CHANGE_PLAN.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lars 2026-04-02 11:46:16 +02:00
parent 6cdc159a94
commit 645967a2ab
5 changed files with 661 additions and 0 deletions

View File

@ -0,0 +1,10 @@
"""
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
__all__ = ['nutrition_part_a']

View File

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

View File

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

View File

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

View File

@ -466,6 +466,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"]