## Ergebnis Die kleinste belastbare Lösung ist **keine Erweiterung der bisherigen generischen DGE/EFSA-Anzeige**, sondern ein fail-closed BLV-Vertrag aus: 1. lokalem, versioniertem BLV-Referenzdatensatz, 2. explizitem, historisierbarem Ernährungs-Referenzprofil, 3. einem Resolver ohne Default-Gruppe, 4. einer additiven Erweiterung der bestehenden Nutrition-Read-API. ### Ist-Zustand - `scripts/health/dashboard_v5/nutrition_contract.py` - Enthält bereits die kanonischen Nährstoff-Keys, Einheiten, Gruppen und Konvertierungen: - `NUTRIENT_CONTRACTS` - `normalize_nutrient_status()` - `normalize_nutrient()` - Das ist die richtige Allowlist-Basis für BLV-Einträge. - `scripts/health/dashboard_v5/read_api.py` - `_daily_nutrients()` aggregiert YAZIO und lokale Anreicherungen mit Status. - `_nutrition_days()` berechnet bereits Mittelwerte **pro dokumentiertem Tag**, nicht über Nulltage. - `_nutrition_day_detail()` trennt Lebensmittel und Supplemente, liefert aber noch keine Nährstoffbeiträge je Lebensmittel. - `dispatch_api()` stellt `/api/v1/nutrition/days` und `/api/v1/nutrition/day/{date}` read-only bereit. - `scripts/health/dashboard_v5/reference_contract.py` - Aktuell nur DGE/EFSA-Präsentationsmetadaten. - `public_reference_context()` verweigert bewusst Zahlenbereiche, weil kein vollständiges Profil existiert. - `database/schema.sql` - Enthält **keine** strukturierte Personentabelle mit Geburtsdatum, Geschlecht oder Schwangerschaft/Stillzeit. - `personal_food_tolerance` ist dafür ungeeignet. - `tests/test_dashboard_v5_sprint6e3_profile.py` - „Profile“ bezeichnet das Dashboard-Buildprofil `health-record-6e`, nicht ein demografisches Patientenprofil. - `scripts/health/dashboard_v5/patient_action_schema.py` - Enthält nur Zeitfelder für Aktionen; ebenfalls kein demografisches Profil. ## Konkreter Minimalentwurf ### 1. Lokale BLV-Daten Neue Datei: `/scripts/health/dashboard_v5/data/blv_nutrient_references_v1.json` Strikter Aufbau: ```json { "dataset_id": "blv_nutrient_references_ch_v1", "contract_version": "blv_nutrient_reference_contract_v1", "source": { "organisation": "Bundesamt für Lebensmittelsicherheit und Veterinärwesen BLV", "title": "...", "edition": "...", "published_on": "YYYY-MM-DD", "retrieved_on": "YYYY-MM-DD", "content_sha256": "..." }, "entries": [ { "nutrient_key": "vitamin.c", "unit": "mg", "group_id": "...", "age": {"minimum_years": 19, "maximum_years_exclusive": 25}, "sex_class": "female", "required_factors": ["pregnancy_status"], "factor_predicates": {"pregnancy_status": "not_pregnant"}, "target": { "kind": "recommended_intake", "lower": 0, "upper": 0, "basis": "per_day" } } ] } ``` Wichtig: - Nur Keys und Einheiten aus `NUTRIENT_CONTRACTS`. - Keine URL- oder Netzabhängigkeit zur Laufzeit. - Duplicate/überlappende Selektoren, unbekannte Faktoren, falsche Einheiten und nicht-endliche Werte beim Laden ablehnen. - Komplizierte Referenzen, etwa gewichts-, PAL- oder energieprozentabhängige Werte, erst freigeben, wenn die nötigen Faktoren und die Berechnung ausdrücklich modelliert sind. Neue Datei: `scripts/health/dashboard_v5/blv_nutrition_reference.py` Konkrete Funktionen: - `load_blv_reference_set()` - `validate_blv_reference_set(payload)` - `load_reference_profile(connection, as_of)` - `validate_profile_factors(factors)` - `resolve_reference_group(reference_set, profile, as_of, nutrient_key)` - `reference_percentages(value, target)` - `build_reference_comparison(...)` Fail-closed-Regeln: - Kein Profil → `profile_status="not_documented"`, keine Zahlenreferenz. - Alter/Geschlecht/Faktor fehlt → `profile_status="incomplete"` plus `missing_factors`. - Kein oder mehr als ein Treffer → `reference_status="not_resolved"` bzw. `"ambiguous"`. - Niemals „allgemeiner Erwachsener“ als Default. ### 2. Historisierbares Profil Additive Tabelle in `database/schema.sql` und in einer kleinen idempotenten Migration, z. B.: `scripts/health/dashboard_v5/blv_reference_profile_schema.py` ```sql CREATE TABLE nutrition_reference_profile_versions ( id INTEGER PRIMARY KEY AUTOINCREMENT, valid_from TEXT NOT NULL, valid_to TEXT, birth_date TEXT, sex_class TEXT CHECK(sex_class IN ('female','male')), factors_json TEXT NOT NULL DEFAULT '{}', source TEXT NOT NULL DEFAULT 'user_documented', recorded_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ); ``` Resolver muss überlappende gültige Profilversionen ablehnen. `factors_json` erhält eine strikte Allowlist, beispielsweise: - `pregnancy_status` - `lactation_status` - `body_weight_kg` - `activity_level` - weitere Faktoren nur zusammen mit einer neuen Vertragsversion `unknown`, fehlend und `not_applicable` dürfen nicht gleichgesetzt werden. Alter wird aus `birth_date` am Referenzstichtag berechnet; keine Schätzung aus Dokumenttexten oder anderen indirekten Daten. ### 3. Bestehende Read-API erweitern Kein neuer Endpoint ist zwingend nötig. Kleinste Änderung: #### `read_api._nutrition_days()` Response auf Version 3 erweitern: ```json { "period": { "from": "...", "to": "...", "calendar_days": 30, "calculation": "arithmetic_mean_per_documented_day" }, "profile": { "status": "resolved", "as_of": "...", "group_factors": {} }, "summary": { "reference_comparisons": { "vitamin.c": { "documented_days": 12, "complete_documented_days": 8, "calendar_days": 30, "mean_per_documented_day": 140, "unit": "mg", "reference": { "source": "BLV", "source_version": "...", "group_id": "...", "as_of": "...", "lower": 90, "upper": 110 }, "percent_of_lower": 155.556, "percent_of_upper": 127.273, "coverage_status": "partial" } } } } ``` Semantik: - Tageswert: `from == to`. - 7 und 30 Tage: bestehende `from`/`to`-Parameter verwenden. - Nenner je Nährstoff = dessen `documented_days`. - Tage ohne Wert werden nicht als null bzw. 0 eingerechnet. - Explizites dokumentiertes Zero zählt als dokumentierter Tag. - Prozentwerte niemals auf 100 begrenzen. - Bei Bereichen getrennt `percent_of_lower` und `percent_of_upper`; bei Punktziel `percent_of_target`. - Keine diagnostischen Begriffe wie „Mangel“, „toxisch“, „gesund“ oder „krank“. #### `read_api._nutrition_day_detail()` Je Nährstoff bzw. zusammenfassend ergänzen: ```json "food_contributions": [ { "food": "...", "value": 25, "unit": "mg", "data_status": "documented_value", "share_of_documented_food_value_percent": 125 }, { "food": "...", "value": null, "unit": "mg", "data_status": "not_reported" } ] ``` Dazu eine gemeinsame interne Funktion, damit Normalisierung nicht dupliziert wird: - `_nutrient_food_evidence(connection, start, end)` - `_aggregate_nutrient_period(evidence, supplement_totals)` Missingness muss auf zwei Ebenen sichtbar bleiben: - pro Nährstoff: dokumentierte/partielle/unbekannte Tage, - pro Lebensmittel: `documented_value`, `documented_zero`, `estimated`, `not_reported`, `unknown`. Supplemente separat halten. Prozentvergleich wahlweise für `food_mean` und `combined_mean`; niemals Supplemente stillschweigend als Lebensmittelbeitrag darstellen. #### `read_api.dispatch_api()` Keine neue Route erforderlich. Bestehende Query- und Zukunftsdatumsprüfung bleibt erhalten. Alternativ wäre ein neuer Endpoint unnötige Vertragsfläche. #### `reference_contract.py` Den bisherigen DGE/EFSA-Platzhalter nicht stillschweigend umdeuten. Entweder: - Nutrition-Zweig auf den neuen BLV-Resolver delegieren, oder - für generische Metrik-Kataloge weiterhin „kein Zahlenband“ liefern und numerische BLV-Auswertung ausschließlich in den Nutrition-Responses ausgeben. ## Konkrete Tests Neue Datei: `tests/test_dashboard_v5_blv_nutrition_references.py` Mindestens folgende Fälle: 1. `test_blv_reference_file_is_local_versioned_and_checksum_valid` 2. `test_blv_loader_rejects_unknown_nutrient_unit_overlap_and_nonfinite_values` 3. `test_profile_resolution_uses_exact_age_on_birthday_and_sex_class` 4. `test_profile_resolution_never_defaults_when_required_factor_is_missing` 5. `test_profile_resolution_fails_closed_for_ambiguous_profile_versions` 6. `test_day_7d_30d_means_use_only_nutrient_documented_days` 7. `test_documented_zero_counts_as_documented_but_missing_day_is_not_zero` 8. `test_reference_percentages_are_not_clamped_above_100` 9. `test_range_contract_returns_percent_of_both_boundaries` 10. `test_food_contributions_expose_documented_estimated_missing_and_unknown` 11. `test_food_and_supplement_contributions_remain_separate` 12. `test_reference_response_contains_source_group_and_as_of` 13. `test_missing_profile_returns_no_numeric_reference` 14. `test_reference_language_is_neutral_and_contains_no_diagnosis` 15. `test_blv_runtime_path_performs_no_external_request` Bestehende Fixture erweitern: `tests/fixtures/dashboard_v5_fixture.py` - synthetische Profilversion, - ein vollständiger, ein partieller und ein vollständig fehlender Nährstofftag, - explizites Zero, - Wert über 100 %, - zwei Lebensmittel, davon eines ohne den betrachteten Nährstoff, - Supplementbeitrag separat. Bestehende Regressionen in: `tests/test_dashboard_v5_sprint6g_nutrition.py` - bestehende Null-vs.-Missingness- und Allowlist-Assertions erhalten, - neue additive Felder und Version 3 prüfen, - keine produktiven Namen oder Daten in Fixtures. ## Verifikation und Zustand - Geprüfte Revision: `79e46c1825f78525f68011f71defd67d1425177c` - Arbeitsbaum vor und nach Prüfung sauber. - Keine Dateien erstellt oder geändert. - Testlauf: - `18 passed` - `1 failed` - Der vorhandene Fehler liegt in `test_mapping_schema_migration_restore_from_fixture_backup`: `_raw_tables()` sortiert jede Tabelle mit `ORDER BY rowid`; mindestens eine aktuelle Fixture-Tabelle besitzt kein `rowid`. Das ist unabhängig vom BLV-Entwurf, sollte aber vor einem Gesamt-Release-Gate korrigiert werden.