# Placeholder Metadata Validation Logic **Version:** 2.0.0 **Generated:** 2026-03-29 **Status:** Normative --- ## Purpose This document defines the **deterministic derivation logic** for all placeholder metadata fields. It ensures that metadata extraction is **reproducible, testable, and auditable**. --- ## 1. Type Classification (`PlaceholderType`) ### Decision Logic ```python def determine_type(key, description, output_type, value_display): # JSON/Markdown outputs are typically raw_data if output_type in [JSON, MARKDOWN]: return RAW_DATA # Scores and percentages are atomic if any(x in key for x in ['score', 'pct', 'adequacy']): return ATOMIC # Summaries and details are raw_data if any(x in key for x in ['summary', 'detail', 'verteilung']): return RAW_DATA # Goals and focus areas (if derived from prompts) if any(x in key for x in ['goal', 'focus', 'top_']): # Check if from KI/Prompt stage if is_from_prompt_stage(key): return INTERPRETED else: return ATOMIC # Just database values # Correlations are interpreted if 'correlation' in key or 'plateau' in key or 'driver' in key: return INTERPRETED # Default: atomic return ATOMIC ``` ### Rules 1. **ATOMIC**: Single values (numbers, strings, dates) from database or simple computation 2. **RAW_DATA**: Structured data (JSON, arrays, markdown) representing multiple values 3. **INTERPRETED**: Values derived from AI/Prompt stages or complex interpretation 4. **LEGACY_UNKNOWN**: Only for existing unclear placeholders (never for new ones) ### Validation - `interpreted` requires evidence of prompt/stage origin - Calculated scores/aggregations are NOT automatically `interpreted` --- ## 2. Unit Inference ### Decision Logic ```python def infer_unit(key, description, output_type, type): # NO units for: if output_type in [JSON, MARKDOWN, ENUM]: return None if any(x in key for x in ['score', 'correlation', 'adequacy']): return None # Dimensionless if any(x in key for x in ['pct', 'ratio', 'balance']): return None # Dimensionless percentage/ratio # Weight/mass if any(x in key for x in ['weight', 'gewicht', 'fm_', 'lbm_']): return 'kg' # Circumferences if 'umfang' in key or any(x in key for x in ['waist', 'hip', 'chest']): return 'cm' # Time if 'duration' in key or 'dauer' in key or 'debt' in key: if 'hours' in description or 'stunden' in description: return 'Stunden' elif 'minutes' in description: return 'Minuten' return None # Unclear # Heart rate if 'rhr' in key or ('hr' in key and 'hrv' not in key): return 'bpm' # HRV if 'hrv' in key: return 'ms' # VO2 Max if 'vo2' in key: return 'ml/kg/min' # Calories if 'kcal' in key or 'energy' in key: return 'kcal' # Macros if any(x in key for x in ['protein', 'carb', 'fat']) and 'g' in description: return 'g' # Default: None (conservative) return None ``` ### Rules 1. **NO units** for dimensionless values (scores, correlations, percentages, ratios) 2. **NO units** for JSON/Markdown/Enum outputs 3. **NO units** for classifications (e.g., "recomposition_quadrant") 4. **Conservative**: Only assign unit if certain from key or description ### Examples ✅ **Correct:** - `weight_aktuell` → `kg` - `goal_progress_score` → `None` (dimensionless 0-100) - `correlation_energy_weight_lag` → `None` (dimensionless) - `activity_summary` → `None` (text/JSON) ❌ **Incorrect:** - `goal_progress_score` → `%` (wrong - it's 0-100 dimensionless) - `waist_hip_ratio` → any unit (wrong - dimensionless ratio) --- ## 3. Time Window Detection ### Decision Logic (Priority Order) ```python def detect_time_window(key, description, semantic_contract, resolver_name): # 1. Explicit suffix (highest confidence) if '_7d' in key: return DAYS_7, certain=True if '_28d' in key: return DAYS_28, certain=True if '_30d' in key: return DAYS_30, certain=True if '_90d' in key: return DAYS_90, certain=True # 2. Latest/current keywords if any(x in key for x in ['aktuell', 'latest', 'current']): return LATEST, certain=True # 3. Semantic contract (high confidence) if '7 tag' in semantic_contract or '7d' in semantic_contract: # Check for description mismatch if '30' in description or '28' in description: mark_legacy_mismatch = True return DAYS_7, certain=True, mismatch_note # 4. Description patterns (medium confidence) if 'letzte 7' in description or '7 tag' in description: return DAYS_7, certain=False # 5. Heuristics (low confidence) if 'avg' in key or 'durchschn' in key: return DAYS_30, certain=False, "Assumed 30d for average" if 'trend' in key: return DAYS_28, certain=False, "Assumed 28d for trend" # 6. Unknown return UNKNOWN, certain=False, "Could not determine" ``` ### Legacy Mismatch Detection If description says "7d" but semantic contract (implementation) says "28d": - Set `time_window = DAYS_28` (actual implementation) - Set `legacy_contract_mismatch = True` - Add to `known_issues`: "Description says 7d but implementation is 28d" ### Rules 1. **Actual implementation** takes precedence over legacy description 2. **Suffix in key** is most reliable indicator 3. **Semantic contract** (if documented) reflects actual implementation 4. **Unknown** if cannot be determined with confidence --- ## 4. Value Raw Extraction ### Decision Logic ```python def extract_value_raw(value_display, output_type, type): # No value if value_display in ['nicht verfügbar', '', None]: return None, success=True # JSON output if output_type == JSON: try: return json.loads(value_display), success=True except: # Try to find JSON in string match = re.search(r'(\{.*\}|\[.*\])', value_display, DOTALL) if match: try: return json.loads(match.group(1)), success=True except: pass return None, success=False # Failed # Markdown if output_type == MARKDOWN: return value_display, success=True # Keep as string # Number if output_type in [NUMBER, INTEGER]: match = re.search(r'([-+]?\d+\.?\d*)', value_display) if match: val = float(match.group(1)) return int(val) if output_type == INTEGER else val, success=True return None, success=False # Date if output_type == DATE: if re.match(r'\d{4}-\d{2}-\d{2}', value_display): return value_display, success=True # ISO format return value_display, success=False # Unknown format # String/Enum return value_display, success=True ``` ### Rules 1. **JSON outputs**: Must be valid JSON objects/arrays, not strings 2. **Numeric outputs**: Extract number without unit 3. **Markdown/String**: Keep as-is 4. **Dates**: Prefer ISO format (YYYY-MM-DD) 5. **Failure**: Set `value_raw = None` and mark in `unresolved_fields` ### Examples ✅ **Correct:** - `active_goals_json` (JSON) → `{"goals": [...]}` (object) - `weight_aktuell` (NUMBER) → `85.8` (number, no unit) - `datum_heute` (DATE) → `"2026-03-29"` (ISO string) ❌ **Incorrect:** - `active_goals_json` (JSON) → `"[Fehler: ...]"` (string, not JSON) - `weight_aktuell` (NUMBER) → `"85.8"` (string, not number) - `weight_aktuell` (NUMBER) → `85` (extracted from "85.8 kg" incorrectly) --- ## 5. Source Provenance ### Decision Logic ```python def resolve_source(resolver_name): # Skip safe wrappers - not real sources if resolver_name in ['_safe_int', '_safe_float', '_safe_json', '_safe_str']: return wrapper=True, mark_unresolved # Known mappings if resolver_name in SOURCE_MAP: function, data_layer_module, tables, kind = SOURCE_MAP[resolver_name] return function, data_layer_module, tables, kind # Goals formatting if resolver_name.startswith('_format_goals'): return None, None, ['goals'], kind=INTERPRETED # Unknown return None, None, [], kind=UNKNOWN, mark_unresolved ``` ### Source Kinds - **direct**: Direct database read (e.g., `get_latest_weight`) - **computed**: Calculated from data (e.g., `calculate_bmi`) - **aggregated**: Aggregation over time/records (e.g., `get_nutrition_avg`) - **derived**: Derived from other metrics (e.g., `protein_g_per_kg`) - **interpreted**: AI/prompt stage output - **wrapper**: Safe wrapper (not a real source) ### Rules 1. **Safe wrappers** (`_safe_*`) are NOT valid source functions 2. Must trace to **real data layer function** or **database table** 3. Mark as `unresolved` if cannot trace to real source --- ## 6. Used By Tracking ### Decision Logic ```python def track_usage(placeholder_key, ai_prompts_table): used_by = UsedBy(prompts=[], pipelines=[], charts=[]) for prompt in ai_prompts_table: # Check template if placeholder_key in prompt.template: if prompt.type == 'pipeline': used_by.pipelines.append(prompt.name) else: used_by.prompts.append(prompt.name) # Check stages for stage in prompt.stages: for stage_prompt in stage.prompts: if placeholder_key in stage_prompt.template: used_by.pipelines.append(prompt.name) # Check charts (future) # if placeholder_key in chart_endpoints: # used_by.charts.append(chart_name) return used_by ``` ### Orphaned Detection If `used_by.prompts` + `used_by.pipelines` + `used_by.charts` are all empty: - Set `orphaned_placeholder = True` - Consider for deprecation --- ## 7. Quality Filter Policy (Activity Placeholders) ### Decision Logic ```python def create_quality_policy(key): # Activity-related placeholders need quality policies if any(x in key for x in ['activity', 'training', 'load', 'volume', 'ability']): return QualityFilterPolicy( enabled=True, default_filter_level="quality", # quality | acceptable | all null_quality_handling="exclude", # exclude | include_as_uncategorized includes_poor=False, includes_excluded=False, notes="Filters for quality='quality' by default. NULL quality excluded." ) return None ``` ### Rules 1. **Activity metrics** require quality filter policies 2. **Default filter**: `quality='quality'` (acceptable and above) 3. **NULL handling**: Excluded by default 4. **Poor quality**: Not included unless explicit 5. **Excluded**: Not included --- ## 8. Confidence Logic ### Decision Logic ```python def create_confidence_logic(key, data_layer_module): # Data layer functions have confidence if data_layer_module: return ConfidenceLogic( supported=True, calculation="Based on data availability and thresholds", thresholds={"min_data_points": 1}, notes=f"Determined by {data_layer_module}" ) # Scores if 'score' in key: return ConfidenceLogic( supported=True, calculation="Based on data completeness for components", notes="Correlates with input data availability" ) # Correlations if 'correlation' in key: return ConfidenceLogic( supported=True, calculation="Pearson correlation with significance", thresholds={"min_data_points": 7} ) return None ``` ### Rules 1. **Data layer placeholders**: Have confidence logic 2. **Scores**: Confidence correlates with data availability 3. **Correlations**: Require minimum data points 4. **Simple lookups**: May not need confidence logic --- ## 9. Metadata Completeness Score ### Calculation ```python def calculate_completeness(metadata): score = 0 # Required fields (30 points) if category != 'Unknown': score += 5 if description and 'No description' not in description: score += 5 if semantic_contract: score += 10 if source.resolver != 'unknown': score += 10 # Type specification (20 points) if type != 'legacy_unknown': score += 10 if time_window != 'unknown': score += 10 # Output specification (20 points) if output_type != 'unknown': score += 10 if format_hint: score += 10 # Source provenance (20 points) if source.data_layer_module: score += 10 if source.source_tables: score += 10 # Quality policies (10 points) if quality_filter_policy: score += 5 if confidence_logic: score += 5 return min(score, 100) ``` ### Schema Status Based on completeness score: - **90-100%** + no unresolved → `validated` - **50-89%** → `draft` - **0-49%** → `incomplete` --- ## 10. Validation Tests ### Required Tests ```python def test_value_raw_extraction(): # Test each output_type assert extract_value_raw('{"key": "val"}', JSON) == {"key": "val"} assert extract_value_raw('85.8 kg', NUMBER) == 85.8 assert extract_value_raw('2026-03-29', DATE) == '2026-03-29' def test_unit_inference(): # No units for scores assert infer_unit('goal_progress_score', ..., NUMBER) == None # Correct units for measurements assert infer_unit('weight_aktuell', ..., NUMBER) == 'kg' # No units for JSON assert infer_unit('active_goals_json', ..., JSON) == None def test_time_window_detection(): # Explicit suffix assert detect_time_window('weight_7d_median', ...) == DAYS_7 # Latest assert detect_time_window('weight_aktuell', ...) == LATEST # Legacy mismatch detection tw, mismatch = detect_time_window('weight_trend', desc='7d', contract='28d') assert tw == DAYS_28 assert mismatch == True def test_source_provenance(): # Skip wrappers assert resolve_source('_safe_int') == (None, None, [], 'wrapper') # Real sources func, module, tables, kind = resolve_source('get_latest_weight') assert func == 'get_latest_weight_data' assert module == 'body_metrics' assert 'weight_log' in tables def test_quality_filter_for_activity(): # Activity placeholders need quality filter policy = create_quality_policy('activity_summary') assert policy is not None assert policy.default_filter_level == "quality" # Non-activity placeholders don't policy = create_quality_policy('weight_aktuell') assert policy is None ``` --- ## 11. Continuous Validation ### Pre-Commit Checks ```bash # Run validation before commit python backend/generate_complete_metadata_v2.py # Check for errors if QA report shows high failure rate: FAIL commit ``` ### CI/CD Integration ```yaml - name: Validate Placeholder Metadata run: | python backend/generate_complete_metadata_v2.py python backend/tests/test_placeholder_metadata_v2.py ``` --- ## Summary This validation logic ensures: 1. **Reproducible**: Same input → same output 2. **Testable**: All logic has unit tests 3. **Auditable**: Clear decision paths 4. **Conservative**: Prefer `unknown` over wrong guesses 5. **Normative**: Actual implementation > legacy description