feat: Placeholder Registry Framework + Part A Nutrition Metrics
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:
parent
6cdc159a94
commit
645967a2ab
10
backend/placeholder_registrations/__init__.py
Normal file
10
backend/placeholder_registrations/__init__.py
Normal 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']
|
||||||
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()
|
||||||
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
|
||||||
|
|
@ -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"]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user