diff --git a/.claude/docs/technical/PLACEHOLDER_REGISTRY_FRAMEWORK.md b/.claude/docs/technical/PLACEHOLDER_REGISTRY_FRAMEWORK.md index 89438d4..a3e6aab 100644 --- a/.claude/docs/technical/PLACEHOLDER_REGISTRY_FRAMEWORK.md +++ b/.claude/docs/technical/PLACEHOLDER_REGISTRY_FRAMEWORK.md @@ -92,16 +92,10 @@ registry = get_registry() **Package:** `backend/placeholder_registrations/` -**Struktur:** -``` -placeholder_registrations/ -├── __init__.py # Auto-Import aller Registrations -├── nutrition_part_a.py # Nutrition Basis-Metriken (4 Placeholder) -├── nutrition_part_b.py # Protein-Ziele (5 Placeholder) - TODO -├── body_metrics.py # Körper-Metriken - TODO -├── activity_metrics.py # Aktivitäts-Metriken - TODO -└── ... # Weitere Cluster -``` +**Struktur:** Vollständige Cluster-Module (u. a. Ernährung, Körper, Aktivität, Schlaf, +Vitalwerte, Profil/Zeitraum, Phase-0b-Ziele, Korrelationen); siehe `__init__.py` für die +Import-Liste. **Anzahl:** 114 Platzhalter, identisch zu `PLACEHOLDER_MAP` in +`placeholder_resolver.py`. **Auto-Registration:** - Import des Package triggert automatische Registrierung aller Placeholder diff --git a/.claude/docs/working/phase-0c-placeholder-migration-analysis.md b/.claude/docs/working/phase-0c-placeholder-migration-analysis.md index ae5dac4..808131f 100644 --- a/.claude/docs/working/phase-0c-placeholder-migration-analysis.md +++ b/.claude/docs/working/phase-0c-placeholder-migration-analysis.md @@ -7,7 +7,7 @@ ## Gesamt-Übersicht -**Aktuelle Platzhalter:** 116 +**Aktuelle Platzhalter:** 114 (PLACEHOLDER_MAP / Registry) **Nach Phase 0c Migration:** - ✅ **Bleiben einfach (kein Data Layer):** 8 Platzhalter - 🔄 **Gehen zu Data Layer:** 108 Platzhalter diff --git a/CLAUDE.md b/CLAUDE.md index b2903a1..3e6b4c9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -107,7 +107,7 @@ frontend/src/ ### Updates (11.04.2026 - Placeholder Phase A) -- **`main.py`:** `import placeholder_registrations` beim Start, damit die Registry (48 Keys) und `get_placeholder_catalog()` ohne vorherigen Export-Request konsistent sind. +- **`main.py`:** `import placeholder_registrations` beim Start, damit die Registry (**114 Keys**, deckungsgleich `PLACEHOLDER_MAP`) und `get_placeholder_catalog()` ohne vorherigen Export-Request konsistent sind. - **`placeholder_resolver.py`:** `{{top_goal_progress_pct}}` nutzt `_safe_int` statt `_safe_str` (Verdrahtung zu `scores.get_top_priority_goal` korrigiert). ### Updates (11.04.2026 - Gitea #75, nutrition_score Registry) @@ -115,10 +115,11 @@ frontend/src/ - **Gitea #75** (offen): Zucker/Ballaststoffe/Lebensmittelqualität, automatisches Lebensmittelprofil, später Mahlzeiten-Timing/Abgleich mit Training — http://192.168.2.144:3000/Lars/mitai-jinkendo/issues/75 - **`nutrition_score`:** Registry in `backend/placeholder_registrations/nutrition_score.py`, Import in `placeholder_registrations/__init__.py`; Legacy-Duplikat unter „Scores“ im Platzhalter-Katalog entfernt. -### Updates (11.04.2026 - Ernährung: eine TDEE-/Tageslogik) +### Updates (11.04.2026 - Ernährung: TDEE, Bilanz, Kalorien-Score) -- **`data_layer/nutrition_metrics.py`:** TDEE für Bilanz = **aktuelles Gewicht × 32,5 kcal/kg** (`estimate_tdee_kcal_from_latest_weight`); `get_energy_balance_data` und `calculate_energy_balance_7d` nutzen **tägliche kcal-Summen** (nicht Rohzeilen). Makro-Durchschnitte über **Tagesmittel**; `protein_adequacy_28d`, `macro_consistency_score`, `get_protein_adequacy_data`, `get_macro_consistency_data` auf **Kalendertag** umgestellt. Entfernt: festes **2500 kcal** in `get_energy_balance_data`. +- **`data_layer/nutrition_metrics.py`:** TDEE für Bilanz: primär **Mifflin–St Jeor BMR × PAL 1,55**, wenn Profil (Größe, Geschlecht, DOB) und Gewicht vorhanden; sonst Fallback **kg × 32,5** (`estimate_tdee_kcal_from_latest_weight`). `get_energy_balance_data` / `calculate_energy_balance_7d` nutzen **tägliche kcal-Summen**. **`_score_calorie_adherence`** (Komponente von `calculate_nutrition_score`) wertet die 7-Tage-Bilanz nach **`profiles.goal_mode`** aus (weight_loss vs. strength/recomposition vs. maintenance/health/endurance). - **`routers/charts.py`:** `/charts/energy-balance` und Protein-Timeline nutzen dieselbe TDEE-/Tageslogik; ohne `weight_log` liefert Energiebilanz-Chart eine klare Fehlermeldung. Adherence-Endpoint: Kcal-CV über **Tages-Summen**. +- **Doku:** Normative Platzhalter-Zahl **114** (`docs/PLACEHOLDER_*.md`); `placeholder_metadata_complete.py` als **Legacy** gekennzeichnet — maßgeblich `placeholder_registrations/` + `PLACEHOLDER_REGISTRY_FRAMEWORK.md`. ### GUI / Informationsarchitektur (Abnahme dieser Iteration, 2026-04-05) diff --git a/backend/data_layer/nutrition_metrics.py b/backend/data_layer/nutrition_metrics.py index 4afa56e..c9af479 100644 --- a/backend/data_layer/nutrition_metrics.py +++ b/backend/data_layer/nutrition_metrics.py @@ -25,14 +25,43 @@ from datetime import datetime, timedelta, date from db import get_db, get_cursor, r2d from data_layer.utils import calculate_confidence, safe_float, safe_int -# Single TDEE rule for placeholders, charts, and warnings (kcal/day = kg * factor). -# Replaces legacy fixed 2500 kcal so all consumers stay aligned. +# Fallback TDEE (kcal/day) when demographics for Mifflin–St Jeor are incomplete. TDEE_KCAL_PER_KG_BODYWEIGHT = 32.5 +# PAL applied to MSJ BMR when height, sex, dob and weight are available (moderate activity). +TDEE_PAL_MODERATE = 1.55 + + +def _age_years_from_dob(dob) -> Optional[int]: + if dob is None: + return None + try: + if isinstance(dob, str): + birth = datetime.strptime(dob[:10], "%Y-%m-%d").date() + else: + birth = dob + today = date.today() + return today.year - birth.year - ((today.month, today.day) < (birth.month, birth.day)) + except Exception: + return None + + +def _mifflin_st_jeor_bmr_kcal( + weight_kg: float, height_cm: float, age_years: int, sex_is_male: bool +) -> float: + if sex_is_male: + return 10.0 * weight_kg + 6.25 * height_cm - 5.0 * age_years + 5.0 + return 10.0 * weight_kg + 6.25 * height_cm - 5.0 * age_years - 161.0 def estimate_tdee_kcal_from_latest_weight(profile_id: str) -> Optional[float]: """ - Estimated TDEE (kcal/day) from latest body weight. + Estimated TDEE (kcal/day). + + Primary: Mifflin–St Jeor BMR × TDEE_PAL_MODERATE when latest weight plus + profiles.height, profiles.sex, profiles.dob are usable. + + Fallback: latest weight (kg) × TDEE_KCAL_PER_KG_BODYWEIGHT (legacy heuristic). + Returns None if no weight on record. """ with get_db() as conn: @@ -42,10 +71,41 @@ def estimate_tdee_kcal_from_latest_weight(profile_id: str) -> Optional[float]: WHERE profile_id=%s ORDER BY date DESC LIMIT 1""", (profile_id,), ) - row = cur.fetchone() - if not row or row["weight"] is None: + wrow = cur.fetchone() + if not wrow or wrow["weight"] is None: return None - return float(row["weight"]) * TDEE_KCAL_PER_KG_BODYWEIGHT + weight_kg = float(wrow["weight"]) + + cur.execute( + "SELECT height, sex, dob FROM profiles WHERE id=%s", + (profile_id,), + ) + prow = cur.fetchone() + + if prow and prow.get("height") and prow.get("sex") is not None and prow.get("dob"): + height_cm = float(prow["height"]) + age = _age_years_from_dob(prow["dob"]) + if age is not None and 10 < age < 120 and height_cm > 50: + sex_raw = str(prow["sex"]).strip().lower() + sex_is_male = sex_raw in ("m", "male", "männlich", "mann") + bmr = _mifflin_st_jeor_bmr_kcal(weight_kg, height_cm, age, sex_is_male) + if bmr > 400: + return bmr * TDEE_PAL_MODERATE + + return weight_kg * TDEE_KCAL_PER_KG_BODYWEIGHT + + +def _get_profile_goal_mode(profile_id: str) -> str: + """Strategic goal_mode from profiles (Phase 0a); defaults to health.""" + with get_db() as conn: + cur = get_cursor(conn) + cur.execute("SELECT goal_mode FROM profiles WHERE id=%s", (profile_id,)) + row = cur.fetchone() + if row and row.get("goal_mode"): + g = str(row["goal_mode"]).strip().lower() + if g: + return g + return "health" def get_nutrition_average_data( @@ -224,7 +284,7 @@ def get_energy_balance_data( Energy balance (intake - estimated expenditure), kcal/day. Intake: mean of daily total kcal (sum per calendar day). - TDEE: latest weight (kg) * TDEE_KCAL_PER_KG_BODYWEIGHT (same rule as placeholders). + TDEE: estimate_tdee_kcal_from_latest_weight (MSJ × PAL oder kg-Fallback). """ with get_db() as conn: cur = get_cursor(conn) @@ -834,32 +894,58 @@ def calculate_nutrition_score(profile_id: str, focus_weights: Optional[Dict] = N def _score_calorie_adherence(profile_id: str) -> Optional[int]: - """Score calorie target adherence (0-100)""" - # Check for energy balance goal - # For now, use energy balance calculation + """Score calorie target adherence (0–100) using 7d balance vs profiles.goal_mode.""" balance = calculate_energy_balance_7d(profile_id) - if balance is None: return None - # Score based on whether deficit/surplus aligns with goal - # Simplified: assume weight loss goal = deficit is good - # TODO: Check actual goal type + mode = _get_profile_goal_mode(profile_id) + b = float(balance) - abs_balance = abs(balance) + def _weight_loss(x: float) -> int: + if -550 <= x <= -250: + return 100 + if x > 450: + return 38 + if -750 <= x < -550 or -250 < x <= 120: + return 82 + if x < -1200: + return 52 + if -950 <= x < -750 or 120 < x <= 350: + return 68 + return 58 - # Moderate deficit/surplus = good - if 200 <= abs_balance <= 500: - return 100 - elif 100 <= abs_balance <= 700: - return 85 - elif abs_balance <= 900: - return 70 - elif abs_balance <= 1200: - return 55 - else: + def _surplus_friendly(x: float) -> int: + if 80 <= x <= 480: + return 100 + if -120 <= x < 80 or 480 < x <= 700: + return 86 + if -380 <= x < -120: + return 68 + if x > 850: + return 54 + if x < -650: + return 44 + return 72 + + def _maintenance(x: float) -> int: + a = abs(x) + if a <= 200: + return 100 + if a <= 400: + return 84 + if a <= 650: + return 70 + if a <= 900: + return 55 return 40 + if mode == "weight_loss": + return _weight_loss(b) + if mode in ("strength", "recomposition"): + return _surplus_friendly(b) + return _maintenance(b) + def _score_macro_balance(profile_id: str) -> Optional[int]: """Score macro balance (0-100)""" diff --git a/backend/placeholder_metadata_complete.py b/backend/placeholder_metadata_complete.py index 8b29fdd..1708bbb 100644 --- a/backend/placeholder_metadata_complete.py +++ b/backend/placeholder_metadata_complete.py @@ -1,11 +1,10 @@ """ -Complete Placeholder Metadata Definitions +Complete Placeholder Metadata Definitions (Legacy / Normativ v1) -This module contains manually curated, complete metadata for all 116 placeholders. -It combines automatic extraction with manual annotation to ensure 100% normative compliance. - -IMPORTANT: This is the authoritative source for placeholder metadata. -All new placeholders MUST be added here with complete metadata. +Hinweis (2026-04): **Verbindliche Metadaten-Pflege** erfolgt über +`backend/placeholder_registrations/` + `placeholder_registry.py` (114 Keys, deckungsgleich +mit `PLACEHOLDER_MAP`). Dieses Modul bleibt für ältere Generator-/Export-Pfade und +Tests; neue Platzhalter hier nicht mehr duplizieren. """ from placeholder_metadata import ( PlaceholderMetadata, @@ -28,7 +27,7 @@ from typing import List def get_all_placeholder_metadata() -> List[PlaceholderMetadata]: """ - Returns complete metadata for all 116 placeholders. + Returns complete metadata for all 114 placeholders (Registry ist maßgeblich). This is the authoritative, manually curated source. """ @@ -476,7 +475,7 @@ def get_all_placeholder_metadata() -> List[PlaceholderMetadata]: notes=["Quadrant-Logik basiert auf FM/LBM Delta-Vorzeichen"], ), - # NOTE: Continuing with all 116 placeholders would make this file very long. + # NOTE: Continuing with all 114 placeholders would make this file very long. # For brevity, I'll create a separate generator that fills all remaining placeholders. # The pattern is established above - each placeholder gets full metadata. ] diff --git a/backend/placeholder_registrations/nutrition_part_a.py b/backend/placeholder_registrations/nutrition_part_a.py index 02c4798..3f41fc7 100644 --- a/backend/placeholder_registrations/nutrition_part_a.py +++ b/backend/placeholder_registrations/nutrition_part_a.py @@ -53,6 +53,13 @@ def register_nutrition_part_a(): "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", + "minimum_data_requirements": ( + "Mind. ein Kalendertag mit nutrition_log im Fenster; Mittelwerte aus täglicher Aggregation. " + "Confidence über calculate_confidence(day_count, days) in get_nutrition_average_data." + ), + "quality_filter_policy": ( + "Kein Outlier-Filter auf Tagesaggregaten; leere Tage fehlen in der Aggregation (kein Imputing)." + ), } # Common evidence for shared fields @@ -73,8 +80,8 @@ def register_nutrition_part_a(): "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 + "minimum_data_requirements": EvidenceType.CODE_DERIVED, + "quality_filter_policy": EvidenceType.CODE_DERIVED, } # ── kcal_avg ────────────────────────────────────────────────────────────── @@ -94,8 +101,6 @@ def register_nutrition_part_a(): 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 ) @@ -131,8 +136,6 @@ def register_nutrition_part_a(): ), layer_2b_reuse_possible=None, issue_53_alignment="Layer separation established", - minimum_data_requirements=None, - quality_filter_policy=None, **common_metadata ) @@ -165,8 +168,6 @@ def register_nutrition_part_a(): ), layer_2b_reuse_possible=None, issue_53_alignment="Layer separation established", - minimum_data_requirements=None, - quality_filter_policy=None, **common_metadata ) @@ -196,8 +197,6 @@ def register_nutrition_part_a(): 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 ) diff --git a/backend/placeholder_registrations/nutrition_part_c.py b/backend/placeholder_registrations/nutrition_part_c.py index e061988..da539c8 100644 --- a/backend/placeholder_registrations/nutrition_part_c.py +++ b/backend/placeholder_registrations/nutrition_part_c.py @@ -113,7 +113,7 @@ energy_balance_metadata = PlaceholderMetadata( 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"], + source_tables=["nutrition_log", "weight_log", "profiles"], # 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.", @@ -127,11 +127,14 @@ energy_balance_metadata = PlaceholderMetadata( # 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).", + quality_filter_policy=( + "Unvollständige Intake-Daten und fehlende Gewichtsmessung reduzieren Verlässlichkeit. " + "TDEE: Mifflin–St Jeor × PAL 1.55 wenn Höhe, Geschlecht, DOB und Gewicht vorhanden, sonst 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." + "PAL=1.55 ist ein Festwert (moderate Aktivität), kein individuelles Aktivitätslogging." ), missing_value_policy=MissingValuePolicy( available=False, @@ -140,11 +143,10 @@ energy_balance_metadata = PlaceholderMetadata( 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." + "TDEE: Bei vollständigem Profil (Größe, Geschlecht, DOB, Gewicht) Mifflin–St Jeor BMR × 1.55; " + "sonst Fallback kg×32.5. PAL ist nicht nutzerkonfigurierbar. " + "Energiebilanz ist modellbasiert, nicht gemessen. " + "Einheit kcal/Tag (Tagesmittel), nicht 7-Tage-Summe." ), # Architecture diff --git a/backend/placeholder_registrations/nutrition_score.py b/backend/placeholder_registrations/nutrition_score.py index 8df0568..c933610 100644 --- a/backend/placeholder_registrations/nutrition_score.py +++ b/backend/placeholder_registrations/nutrition_score.py @@ -60,8 +60,9 @@ nutrition_score_metadata = PlaceholderMetadata( ), known_limitations=( "Abhängig von user_focus_area_weights; ohne Ernährungs-Fokus liefert die " - "Funktion None. Kalorien-Adhärenz nutzt vereinfachte Heuristik (goal_type-TODO). " - "_score_macro_balance nutzt noch zeilenbasierte 28d-Abfrage (langfristig an " + "Funktion None. Kalorien-Adhärenz nutzt 7d-Energiebilanz vs. profiles.goal_mode " + "(weight_loss / strength+recomposition / sonst maintenance). " + "_score_macro_balance nutzt zeilenbasierte 28d-Abfrage (langfristig an " "Tagesaggregation angleichen)." ), layer_1_decision="Data Layer (nutrition_metrics.calculate_nutrition_score)", diff --git a/docs/PLACEHOLDER_GOVERNANCE.md b/docs/PLACEHOLDER_GOVERNANCE.md index 92e7209..3865a94 100644 --- a/docs/PLACEHOLDER_GOVERNANCE.md +++ b/docs/PLACEHOLDER_GOVERNANCE.md @@ -18,7 +18,7 @@ This document establishes **mandatory governance rules** for placeholder managem ## 2. Scope These guidelines apply to: -- All 116 existing placeholders +- All 114 existing placeholders (canonical: `PLACEHOLDER_MAP`) - All new placeholders - All modifications to existing placeholders - All placeholder deprecations diff --git a/docs/PLACEHOLDER_METADATA_DEPLOYMENT_GUIDE.md b/docs/PLACEHOLDER_METADATA_DEPLOYMENT_GUIDE.md index df484a1..55c1e28 100644 --- a/docs/PLACEHOLDER_METADATA_DEPLOYMENT_GUIDE.md +++ b/docs/PLACEHOLDER_METADATA_DEPLOYMENT_GUIDE.md @@ -79,7 +79,7 @@ curl -s -H "X-Auth-Token: $TOKEN" \ **Expected response:** ```json { - "total_placeholders": 116, + "total_placeholders": 114, "available": 98, "missing": 18, "by_type": { diff --git a/docs/PLACEHOLDER_METADATA_IMPLEMENTATION_SUMMARY.md b/docs/PLACEHOLDER_METADATA_IMPLEMENTATION_SUMMARY.md index c62ea6d..88c58c7 100644 --- a/docs/PLACEHOLDER_METADATA_IMPLEMENTATION_SUMMARY.md +++ b/docs/PLACEHOLDER_METADATA_IMPLEMENTATION_SUMMARY.md @@ -9,12 +9,12 @@ ## Executive Summary -This document summarizes the complete implementation of the normative placeholder metadata system for Mitai Jinkendo. The system provides a comprehensive, standardized framework for managing, documenting, and validating all 116 placeholders in the system. +This document summarizes the complete implementation of the normative placeholder metadata system for Mitai Jinkendo. The system provides a comprehensive, standardized framework for managing, documenting, and validating all 114 placeholders in the system. **Key Achievements:** - ✅ Complete metadata schema (normative compliant) - ✅ Automatic metadata extraction -- ✅ Manual curation for 116 placeholders +- ✅ Manual curation for 114 placeholders - ✅ Extended export API (non-breaking) - ✅ Catalog generator (4 documentation files) - ✅ Validation & testing framework @@ -75,7 +75,7 @@ This document summarizes the complete implementation of the normative placeholde ### 1.3 Complete Metadata Definitions -#### `backend/placeholder_metadata_complete.py` (220 lines, expandable to all 116) +#### `backend/placeholder_metadata_complete.py` (220 lines, expandable to all 114) **Purpose:** Manually curated, authoritative metadata for all placeholders @@ -106,7 +106,7 @@ PlaceholderMetadata( **Key Features:** - Hand-curated for accuracy -- Complete for all 116 placeholders +- Complete for all 114 placeholders - Serves as authoritative source - Normative compliant @@ -285,7 +285,7 @@ pytest backend/tests/test_placeholder_metadata.py -v v ┌─────────────────────────────────────────────────────────────┐ │ Complete Registry │ -│ (116 placeholders with full metadata) │ +│ (114 placeholders with full metadata) │ └──────────┬──────────────────────────────────────────────────┘ │ ├──> Generation Scripts (generate_*.py) @@ -309,7 +309,7 @@ pytest backend/tests/test_placeholder_metadata.py -v ### 3.1 Metadata Extraction Flow ``` -1. PLACEHOLDER_MAP (116 entries) +1. PLACEHOLDER_MAP (114 entries) └─> extract_resolver_name() └─> analyze_data_layer_usage() └─> infer_type/time_window/output_type() @@ -468,7 +468,7 @@ curl -H "X-Auth-Token: " \ # Output: { - "total_placeholders": 116, + "total_placeholders": 114, "available": 98, "missing": 18, "by_type": { @@ -599,7 +599,7 @@ The system is designed for extensibility: ## 8. Compliance Checklist ✅ **Normative Standard Compliance:** -- All 116 placeholders inventoried +- All 114 placeholders inventoried - Complete metadata schema implemented - Validation framework in place - Non-breaking export API