what_to_wear/_bmad-output/planning-artifacts/epics.md
Nora 2686f253d6 epics: Epic-/Story-Zerlegung final (5 Epics, 23 Stories) — Party-Mode + Readiness
Phase 4 der AI-Dev-Method abgeschlossen:
- 5 Epics, 23 Stories, alle mit Given/When/Then + FR-/AD-Referenzen.
- Pflicht-Party-Mode (4 Linsen Architekt/Dev/PM/UX): Draft reorganisiert —
  '15-Minuten-Wow'-Pfad (S-2) landet Ende Epic 2; grosse Epic-1-Stories gesplittet.
- Readiness-Check (adversarial): READY-MIT-AUFLAGEN, alle 34 FR / 10 NFR / 24 AD
  abgedeckt; 3 Auflagen eingearbeitet (Plattform-Naht, NFR-1-Assert, Reason-Keys).
- Test-Umgebung provisioniert (.venv Py3.13 + phcc 0.13.225 = HA 2025.3.4).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 13:12:33 +00:00

48 KiB
Raw Blame History

stepsCompleted inputDocuments status
step-01-validate-prerequisites
step-02-design-epics
step-03-create-stories
party-mode-4-lenses
step-04-final-validation
_bmad-output/planning-artifacts/prd.md
_bmad-output/planning-artifacts/prd-addendum.md
_bmad-output/planning-artifacts/architecture/architecture-what-to-wear-2026-07-11/ARCHITECTURE-SPINE.md
final

What to Wear (WTW) — Epic Breakdown

Overview

Dieses Dokument zerlegt PRD (Rev. 3, final) und Architektur-Spine (final, 24 ADs) in implementierbare Epics und Stories. Ordnungsprinzip: Nutzerwert je Epic; innerhalb eines Epics strikt sequenzielle, einzeln abschließbare Stories ohne Vorwärts-Abhängigkeit, jede mit grüner Suite abschließbar (DEV-METHOD Story-Loop). Die Architektur (Pipes-and-Filters-Kern logic/ hass-frei, Ports-and-Adapters-Schale) erlaubt es, den deterministischen Kern rein und testbar Story für Story aufzubauen, bevor die HA-Adapter ihn sichtbar machen. Struktur nach Party-Mode-Runde (4 Linsen: Architekt/Dev/PM/UX) überarbeitet — der »15-Minuten-Wow«-Pfad (S-2/JTBD-3) landet vollständig am Ende von Epic 2.

Konventionen für alle UI-Stories: Jede Story, die einen Flow/Entity/Service einführt, liefert ihre en/de-Übersetzungsschlüssel inkrementell mit (kein strings.json, AD-13); Story 5.1 validiert nur Vollständigkeit + hassfest. Das normative Options-/Data-Schema (AD-23) inkl. Defaults und TO_REDACT entsteht einmalig in Story 1.1 (const.py); spätere Stories verdrahten nur UI dagegen, bauen es nie neu.

Requirements Inventory

Functional Requirements

  • FR-1.1 HACS-Custom-Integration installierbar (Domain what_to_wear); Distribution braucht öffentliches GitHub-Repo (E-3).
  • FR-1.2 Config-Flow: weather.*-Entität per Selector + sofortiger Test-Abruf; 4 unterscheidbare, übersetzte Fehlerfälle (keine Entität / keine Prognose / reicht nicht bis Zieldatum / temporär).
  • FR-1.3 Config-Flow: Kälteempfinden-Offset (2…+2, Default 0, Richtung im Label); Sprachbindung zweigeteilt (dynamisch je Berechnung / registriert beim Setup); Entity-ID sprachneutral fix.
  • FR-1.4 Options-Flow: Schwellwerte + Umschaltzeitpunkt (jede Schwelle einzeln, validiert, monotone Bandgrenzen), LLM-Ton (Key+Provider, maskiert), Wetter-Entität wechseln (mit Test-Abruf).
  • FR-1.5 Config-Flow-Option »Beispiel-Kleiderschrank anlegen« (12 Stücke aus Addendum §8, sofort editier-/löschbar).
  • FR-1.6 Genau ein Config-Entry (single_config_entry); Mehrfach-Instanzen = v1.1.
  • FR-2.1 Stücke als Sub-Entries anlegen/bearbeiten/löschen (native HA-UI). 3 Pflichtfelder (Name, Kategorie, Wärmegrad 15) + optionale Felder (wasserdicht, winddicht, Sonnenschutz, Formalität, Temperaturbereich, aktiv).
  • FR-2.2 Stücke mit aktiv = nein werden nie empfohlen.
  • FR-2.3 Effektiv leerer Schrank verhindert die Empfehlung nicht (nennt Requirements als Kategorien + Ursache).
  • FR-2.4 Doku warnt: Entfernen der Integration löscht das Inventar; HA-Backups sichern es mit.
  • FR-3.1 Tägliche und (falls verfügbar) stündliche Prognosen der Entität über HA-Forecast-Mechanismus; Herkunft/Vorrang/Mindestabdeckung nach Addendum §5.
  • FR-3.2 Zieldatum: vor Umschaltzeitpunkt heute, danach morgen (lokales Kalenderdatum, DST-fest); Zieldatum + Erstellzeitpunkt als Attribute.
  • FR-3.3 Fehlende Prognosefelder als »fehlend« (nie 0); außerhalb Plausibilität = fehlend.
  • FR-3.4 Keine Prognose fürs Zieldatum → erklärender Fehlzustand (kein Absturz, keine stumme Null).
  • FR-4.1 Deterministische Requirements aus der Normprognose inkl. Basis-Outfit; Regeltabelle (Addendum §3) verbindlicher Default; Schwellwerte konfigurierbar.
  • FR-4.2 Offset verschiebt ausschließlich die °C-Schwellen um +2 °C je Stufe (Richtung: +1 = früher wärmer); Regen/Wind/UV unberührt.
  • FR-4.3 Temperatur-Regeln auf gefühlte Temperatur im Morgenfenster; Degradations-Tabelle (Addendum §3a) verbindlich; Datenlage-Hinweis nur bei betroffenem muss/soll.
  • FR-4.4 Tagesmaximum ≥ 2 Wärmebänder über Morgenfenster → Tagesgang-Hinweis.
  • FR-5.1 Matcher baut vollständiges Outfit (Basis + Schichten bis Wärmebedarf + Zusatzstücke); Außenschicht-Attribute auf äußerster Schicht; Regeln nach Addendum §4; deterministisch stabil.
  • FR-5.2 Unerfüllbare muss/soll-Requirements → benannte Lücken; Mapping Requirement→Kategorie (Addendum §4a).
  • FR-5.3 Jedes empfohlene Stück trägt eine menschenlesbare Begründung.
  • FR-6.1 LLM-Ton default aus; aktivierbar nur per eigenem Key (OpenAI/Anthropic).
  • FR-6.2 LLM erhält nur die strukturierte Empfehlung + Zielsprache; nie Fotos/Koordinaten/Entity-IDs; nur umformulieren (Prompt-Härtung + Ausgabe-Validierung).
  • FR-6.3 LLM-Fehler/Timeout/kein Key → deterministischer Regeltext; Empfehlung fällt nie aus.
  • FR-6.4 Key nie in Logs/Attributen/Events/Diagnostics; im HA-Config-Storage; Doku transparent (at-rest/Backups).
  • FR-6.5 Beim Aktivieren zeigt der Options-Flow die übertragenen Daten (Drittland möglich) + Provider-Bedingungen; README »Datenschutz« de+en.
  • FR-6.6 Stücknamen als Untrusted Data; LLM-Ausgabe gegen harte Kriterien validiert (Fließtext, ≤700, jeder Stückname, jede Lücke); Verstoß → Fallback; Injection-Testkatalog.
  • FR-7.1 Sensor sensor.what_to_wear (State ≤255; Attribute: Stücke/Requirements/Lücken/Kennwerte/Quelle/Abrufzeit/Zieldatum/Erstellzeit/Datenlage); Größengrenzen 16 KB / 30 Stücke; LLM-Text nie in Attributen.
  • FR-7.2 Service what_to_wear.recommend erzwingt Neuberechnung, liefert Service-Response + feuert Event; Blueprints nutzen die Response.
  • FR-7.3 Read-only Lovelace-Karte im Integrationsordner, Storage-Mode automatisch registriert, YAML-Mode dokumentiert; zeigt Empfehlung/Zieldatum/Stücke/Lücken.
  • FR-7.4 Zwei Blueprints (Push: notify-Ziel + Uhrzeit + »nur bei Änderung/Lücke/Warnlage«; TTS: media_player + Auslöser); beide rufen recommend vorher auf.
  • FR-7.5 Aktualisierung stündlich + bei Neustart + exakt am Umschaltzeitpunkt; letzte Berechnung > 6 h → als veraltet gekennzeichnet (Sensor und Karte).
  • FR-8.1 UI + Empfehlungstexte in de + en; Auswahl folgt HA-Sprache.

NonFunctional Requirements

  • NFR-1 Keine externen Netzaufrufe außer optional aktiviertem LLM; keine Telemetrie.
  • NFR-2 Alle IO-Pfade async; Event-Loop nie blockiert.
  • NFR-3 Unit-Tests (Normalisierung/Regeln/Matcher mit Fixtures) + Integrationstests (phcc) + Injection-Katalog + E2E vor Release.
  • NFR-4 Min-HA ≥ 2025.3; gegen reale APIs verifiziert; in CI gegen die Minimalversion getestet.
  • NFR-5 HACS/Brands-Konformität (Repo, hacs.json, manifest, brands-PR, hassfest + HACS-Action).
  • NFR-6 API-Keys nur im HA-Config-Storage; nie in Logs/State/Attributen/Events/Diagnostics.
  • NFR-7 Empfehlung ohne LLM < 5 s; mit LLM < 30 s, sonst Fallback.
  • NFR-8 Jede Empfehlung aus den Sensor-Attributen vollständig nachvollziehbar.
  • NFR-9 LICENSE (Apache-2.0) + NOTICE; Dependencies kompatibel; gebündelte JS behalten Lizenzhinweise.
  • NFR-10 Config-/Sub-Entry-Schema ab v1 versioniert; Migrationspfad ohne Datenverlust.

Additional Requirements

Aus dem Architektur-Spine (kein Starter-Template; greenfield; Pipes-and-Filters-Kern in Ports-and-Adapters-Schale). Verbindliche Umsetzungs-Constraints (AD = Architektur-Entscheidung):

  • AD-1/AD-24 logic/* importiert kein homeassistant.* (auch kein dt_util); Zeit-Parsing im Adapter (weather/ha_entity.py), Kern nutzt zoneinfo. Durchsetzung: Import-Scan-Test.
  • AD-2/AD-7 Fehlermodell status ∈ {ok, fehler_prognose} mit exaktem Prädikat; Coordinator ist einziger Mutator (asyncio.Lock); Setup wartet nicht aufs Wetter (Sensor immer da, nie data is None); Stale-Besitz beim Sensor.
  • AD-3 Zieldatum halboffen (morgen ⇔ Uhrzeit ≥ Umschaltzeit); async_track_time_change; DST-/TZ-Listener.
  • AD-4 SI-Normmodell; Einheiten aus Quell-Entität lesen; Plausibilität nach Konvertierung; NormField(value|None, source, note).
  • AD-6 Keine SDK-Dependencies (requirements: []); LLM raw-HTTP mit URL-Konstanten, allow_redirects=False, Timeout 18 s, Body-Cap 64 KB, 1 Versuch.
  • AD-8 entry.data={weather_entity_id}, entry.options=Schema AD-23, Stücke=Subentries; VERSION=1; Robustheits-Contract beim Lesen.
  • AD-9 ItemSubentryFlow mit async_step_user + async_step_reconfigure (Kompat-Accessor 2025.3/2025.4); Beispiel-Set nur im initialen user-Step.
  • AD-10 2025.3-Kompat-Leitplanken (kein OptionsFlowWithReload/sync register_static_path/dict-lovelace-Zugriff); CI-Matrix min+latest.
  • AD-11 Service recommend OPTIONAL-Response, garantiert frisch (Coordinator-Lock + await async_refresh), Event immer.
  • AD-12 Karten-Registrierung (StaticPathConfig + Lovelace-Ressource idempotent, YAML-Mode-Erkennung, Deregistrierung); Karte XSS-sicher (textContent), Platzhalter bei fehlender Entität.
  • AD-13 Kein strings.json; translations/{en,de}.json (en vollständig, hassfest-valid); dynamische Texte in logic/texts.py; Karte rendert nur gelieferte Strings.
  • AD-14/AD-19/AD-20 Frozen Recommendation; eine to_payload() mit englischer Key-Liste + 4 Projektionen (state/attributes/event/response); Größen-Enforcement + Shedding-Kaskade nur in Projektionen; Textmodell (short/full/llm).
  • AD-15 Stabile Entity-ID (unique_id + self.entity_id vor async_add_entities); manifest-Pflichtfelder.
  • AD-16 LLM-Phraser: JSON-escaped Untrusted-Block, Validierung (a)(e); tone+data_notes-Sichtbarkeit; dokumentiertes Rest-Risiko.
  • AD-17 Forecast-Konsum (get_forecasts blocking+return_response), Feature-Check, Fehler-Taxonomie, Morgenfenster-Prädikat, hourly-only-Ableitung, Condition-Mapping.
  • AD-18 Test-Architektur: met.no-/°F-/DST-/Grenzwert-/Injection-Fixtures; Start-ohne-Entität-Test; E2E-Release-Gate.
  • AD-21 Requirement-Registry (Enum, einzige Key-Quelle); rules liefert kanonisch gemergte Map (max-Priorität, warmth-Merge).
  • AD-22 Item-Schema-Registry (englische Keys/Kategorie-Enum; temp_range-Semantik; Überlappung inklusiv; Bandzuordnung ungerundet).
  • AD-23 Normatives Options-Schema (flach, const.py); Offset genau einmal in rules.py; TO_REDACT.
  • Distribution: GitHub-Repo als HACS-Vertriebskanal (E-3-Mirror); brands-PR; CI-Sicherheit (permissions: contents: read, Actions möglichst SHA-gepinnt).

UX Design Requirements

Keine separate UX-Design-Spezifikation (bmad-ux) vorhanden. Die UI ist bewusst minimal und vollständig aus PRD/Architektur ableitbar: native HA-Config-/Options-/Subentry-Flows (keine Custom-UI), eine read-only Lovelace-Karte, zwei Blueprints. Karten-Visuals sind bewusst Story-Ebene (Deferred im Spine); der Daten-/Anzeigepfad ist über AD-12/13/19/20 fixiert.

FR Coverage Map

  • FR-1.1 → Story 1.1 (Skeleton/Manifest) + Story 5.2 (HACS-Konformität)
  • FR-1.2 → Story 1.8 (Config-Flow Wetter-Entität + Test-Abruf)
  • FR-1.3 → Story 1.8 (Offset im Config-Flow) + Story 5.1 (Sprachbindung/Übersetzungen)
  • FR-1.4 → Story 3.1 (Options-Flow) + Story 4.1 (LLM-Teil)
  • FR-1.5 → Story 2.2 (Beispiel-Kleiderschrank)
  • FR-1.6 → Story 1.1 (single_config_entry im Manifest) + Story 1.8 (Flow)
  • FR-2.1, FR-2.2 → Story 2.1 (Subentry-CRUD, aktiv-Filter im Matcher 1.4)
  • FR-2.3 → Story 1.4 (leerer Schrank im Matcher) + Story 1.9 (Sensor-Anzeige)
  • FR-2.4 → Story 3.2 (Verhalten) + Story 5.3 (README-Löschwarnung)
  • FR-3.1, FR-3.3 → Story 1.2 (Normalizer)
  • FR-3.2 → Story 1.7 (Zieldatum/Coordinator)
  • FR-3.4 → Story 1.7 (Fehlzustand Coordinator) + Story 1.9 (Sensor-Anzeige)
  • FR-4.14.4 → Story 1.3 (Regel-Engine); Konfigurierbarkeit: Schema 1.1 + Wiring 3.1
  • FR-5.15.3 → Story 1.4 (Matcher) + Story 1.5 (Gründe im Text)
  • FR-6.16.6 → Epic 4 (LLM-Ton)
  • FR-7.1 → Story 1.5 (Payload) + Story 1.9 (Sensor)
  • FR-7.2 → Story 2.4 (Service/Event)
  • FR-7.3 → Story 2.3 (Karte)
  • FR-7.4 → Story 2.5 (Blueprints)
  • FR-7.5 → Story 1.7 (Recompute-Trigger) + Story 1.9 (Sensor-Stale) + Story 2.3 (Karten-Stale)
  • FR-8.1 → Story 5.1 (i18n)
  • NFR-1/2 → Story 1.1 (requirements: []/Guard) + Story 5.3 (C-2-Assert), durchgängig
  • NFR-3 → Story 1.1 (Test-Fundament) + je Story + Story 5.3 (E2E)
  • NFR-4 → Story 5.2 (CI-Matrix min+latest)
  • NFR-5/9 → Story 5.2 (HACS/Brands/Lizenz)
  • NFR-6 → Story 4.4 (Diagnostics) + Story 4.1 (Key-Schutz)
  • NFR-7 → Story 1.7 (Timing-Assert ohne LLM) + Story 4.3 (LLM-Budget) + Story 5.3 (E2E)
  • NFR-8 → Story 1.5 (Payload/Shedding schützt Keys)
  • NFR-10 → Story 3.2 (Migration/Versionierung)

Epic List

Epic 1: Deterministische Empfehlung sichtbar (Wetter → Outfit → Sensor)

Nach diesem Epic installiert ein Nutzer WTW, wählt seine Wetter-Entität und sieht auf sensor.what_to_wear eine echte, erklärbare Outfit-Empfehlung fürs Zieldatum — inklusive korrektem »leerer Schrank«-Verhalten, wenn noch keine Stücke gepflegt sind. Enthält den gesamten reinen Kern (logic/), Wetter-Adapter, Coordinator, Config-Flow und Sensor. FRs: FR-1.1, FR-1.2, FR-1.3 (Offset), FR-1.6, FR-2.3, FR-3.13.4, FR-4.14.4, FR-5.15.3, FR-7.1, FR-7.5.

Epic 2: Der 15-Minuten-Wow (Kleiderschrank → Karte → Push)

Der vollständige »Wow«-Pfad (S-2/JTBD-3): der Nutzer verwaltet echte Stücke, legt beim Setup ein Beispiel-Set an, sieht das Outfit auf einer Dashboard-Karte und bekommt es aufs Handy. Enthält Subentry-CRUD, Beispiel-Set, Lovelace-Karte, Service+Event, Push- und TTS-Blueprints. FRs: FR-1.5, FR-2.1, FR-2.2, FR-7.2, FR-7.3, FR-7.4.

Epic 3: Anpassung & Robustheit

Volle Konfigurierbarkeit über den Options-Flow (Schwellen, Umschaltzeit, Wetter-Entität-Wechsel) und ein verlustfreier Migrationspfad. Baut den Options-Flow (Scaffold), den spätere Stories erweitern. FRs: FR-1.4 (Schwellen/Umschaltzeit/Entität), FR-2.4 (Datenverlust-Verhalten), NFR-10.

Epic 4: Optionaler LLM-Ton (Bring-your-own-Key)

Wer will, aktiviert mit eigenem Key einen sprachlich schöneren Ton — opt-in, transparent, sicher (Prompt-Härtung, Ausgabe-Validierung, Key-Schutz, Diagnostics-Redaction), mit garantiertem Regeltext-Fallback. FRs: FR-6.16.6, NFR-6.

Epic 5: Internationalisierung & Release-Reife

Vollständige Zweisprachigkeit, CI-Matrix (min+latest HA), HACS-/Brands-/Lizenz-Konformität, README mit Pflichtabschnitten und E2E-Verifikation — HACS-ready. FRs: FR-1.3 (Sprachbindung), FR-2.4 (README), FR-8.1, NFR-3, NFR-4, NFR-5, NFR-9.


Epic 1: Deterministische Empfehlung sichtbar (Wetter → Outfit → Sensor)

Ziel: ein installierbarer, testbarer vertikaler Durchstich — vom reinen Rechenkern bis zum sichtbaren Sensor. Stories 1.11.5 bauen den hass-freien Kern (rein, mit Fixtures testbar), 1.61.9 machen ihn in HA sichtbar. Keine Story hängt von einer späteren ab.

Story 1.1: Projekt-Skeleton, Datenmodell, Options-Schema & Schichtungs-Guard

As a Entwickler, I want ein installierbares Integrations-Gerüst mit den eingefrorenen Kern-Datenmodellen, dem vollständigen Options-Schema und einem Import-Schichtungs-Test, So that alle folgenden Stories auf einem stabilen, konventionskonformen und drift-freien Datenvertrag aufsetzen.

Acceptance Criteria:

Given ein leeres custom_components/what_to_wear/ When das Skeleton angelegt ist Then existieren manifest.json (domain what_to_wear, version, single_config_entry: true, config_flow: true, iot_class: calculated, integration_type: service, requirements: [], codeowners, documentation, issue_tracker, dependencies: [http, frontend, lovelace]), hacs.json (name, homeassistant: "2025.3.0"), const.py, logic/model.py und logic/__init__.py

Given logic/model.py When es geladen wird Then definiert es die frozen Dataclasses NormField, RawForecast, NormForecast, Requirement, Item, Outfit, Recommendation und die Enums RequirementKey (AD-21: base_top, base_bottom, base_shoes, warmth, waterproof_outer, windproof_outer, sturdy_shoes, hat, gloves, scarf, sun_protection, hint_heat, hint_thunderstorm, hint_layering), Category (AD-22: top, sweater, jacket, bottom, shoes, head, hands, neck, accessory), Priority (must, should, may) And Recommendation enumeriert vollständig die Felder für to_payload() (AD-19): status, target_date, target_label, created_at, forecast_fetched_at, language, tone, short_text, full_text, llm_text, items, items_truncated, requirements, gaps, metrics, data_notes, source, signature, changed, stale, alert, gaps_count — wobei changed/stale reine Platzhalter (Default) sind, die HA-seitige Owner (Coordinator/Sensor) füllen; signature wird im Kern berechnet

Given das normative Options-/Data-Schema (AD-23) When const.py geladen wird Then enthält es einmalig das vollständige, flache, englische Schema mit Typ/Bereich/Default (warmth_band_limits, heat_threshold, rain_prob_should/_must, rain_amount_should/_must, gust_should, wind_proxy_should, uv_should, cold_sensitivity_offset, switchover_time, llm_enabled, llm_provider, llm_api_key, llm_model), die Konstante TO_REDACT = {llm_api_key, weather_entity_id}, die Timeout-Konstanten und die LLM-URL-Konstanten — spätere Stories verdrahten nur dagegen

Given die Konvention AD-1 (Kern importiert kein homeassistant.*) When die Test-Suite läuft Then scannt ein Pflicht-Test alle Dateien unter logic/ und schlägt fehl, sobald ein homeassistant- oder dt_util-Import auftaucht; Recommendation-Mutation wirft FrozenInstanceError (AD-14); pytest läuft grün

Story 1.2: Forecast-Normalizer (SI-Normmodell mit Missingness)

As a Nutzer, I want dass beliebige Wetterprognosen verlässlich in ein einheitliches SI-Modell fürs Zieldatum überführt werden, So that die Regeln unabhängig von Einheiten und Datenlücken korrekt rechnen.

Acceptance Criteria:

Given ein RawForecast mit TZ-awaren Zeitstempeln, Einheiten-Angaben und dem IANA-TZ-Namen (Adapter-Vertrag AD-24) When normalize.py ihn verarbeitet Then liefert es eine NormForecast mit den §5-Feldern, je als NormField(value|None, source, note); Herkunft daily/hourly/derived; Einheiten explizit nach SI konvertiert (°F→°C, mph/m/s→km/h, inch→mm); fehlt die Einheiten-Angabe zu einem Wert, gilt das Feld als fehlend (nie geraten, AD-4)

Given ein Wert außerhalb der Plausibilitätsgrenzen (§5) nach SI-Konvertierung When normalisiert wird Then gilt das Feld als fehlend + Vermerk (fehlend ≠ 0, FR-3.3)

Given hourly-Einträge im 3-h-Raster (met.no-Realität) When das Morgenfenster [06:00, 09:00) über die Kern-Funktion morning_window_coverage() ausgewertet wird Then gilt es als abgedeckt, sobald ≥ 1 Eintrag im Fenster liegt; Fensterwert = Minimum der Fenster-Einträge (AD-17); fehlt daily ganz, werden Tagesaggregate deterministisch aus den hourly-Einträgen abgeleitet (min/max/Summe/Maximum, source=derived)

Given Fixtures für met.no-Profil (kein apparent_temperature), °F/mph-Einheiten, DST-Tag (23/25 h), Teilabdeckung When die Unit-Tests laufen Then sind alle grün und decken Einheiten, Missingness, Plausibilität, Morgenfenster und DST ab (AD-18)

Story 1.3: Regel-Engine (Requirements aus der Normprognose)

As a Nutzer, I want dass aus dem Wetter deterministisch nachvollziehbare Anforderungen abgeleitet werden, So that jede Empfehlung erklärbar auf konkreten Wetterbedingungen beruht.

Acceptance Criteria:

Given eine NormForecast und die Schwellwerte aus dem const.py-Schema (Defaults; per Argument übergeben) When rules.py läuft Then liest es die Schwellen ausschließlich aus dem übergebenen Schema-Objekt (keine Hardcodes außer den const.py-Defaults) — so wirkt der spätere Options-Flow (Story 3.1) ohne Änderung an rules.py (FR-4.1) — und erzeugt immer das Basis-Outfit (base_top/bottom/shoes, must) plus die §3-Requirements als kanonisch gemergte Map key → (priority, params): gleicher Key ⇒ max(priority), warmth ⇒ ein Eintrag mit N = max(band_N, 4 bei Schnee), hint_* dedupliziert (AD-21)

Given ein Kälteempfinden-Offset ≠ 0 When die Bänder bewertet werden Then verschiebt rules.py alle °C-Schwellen (inkl. heat_threshold) genau einmal um 2 · offset (+1 ⇒ +2 °C, früher wärmer); Regen/Wind/UV unberührt (AD-23, FR-4.2)

Given fehlende Felder When die Regeln greifen Then folgt das Verhalten exakt der Degradations-Tabelle §3a; ein Datenlage-Hinweis entsteht nur bei betroffenem muss/soll-Requirement (FR-4.3); Tagesmaximum ≥ 2 Bänder über Morgenfenster → hint_layering (FR-4.4)

Given Grenzwert-Fixtures (halboffene Bänder auf ungerundeten SI-Werten: 7.9999 °C; Offset +1 verschiebt 7.5-°C-Morgen von Band 4 nach Band 3; Frost+Schnee+Starkregen) When die Unit-Tests laufen Then sind sie grün und die Merge-Semantik liefert genau einen waterproof_outer (must) und einen warmth:5

Story 1.4: Matcher (vollständiges Outfit + Lücken)

As a Nutzer, I want dass aus meinen Stücken ein vollständiges, wetterpassendes Outfit zusammengestellt und fehlende Stücke benannt werden, So that ich sehe, was ich rauslegen soll — und was mir fehlt.

Acceptance Criteria:

Given eine gemergte Requirement-Map und eine Liste aktiver Items When matcher.py läuft Then baut es ein vollständiges Outfit nach §4 (Basis + Schichten Oberteil→Pullover→Jacke bis Schichtsumme ≥ N, Hosen-/Schuh-Mindestwärme, Außenschicht-Attribute auf äußerster Schicht) und liefert je Stück eine sprachneutrale Begründung als Grund-Key (erfülltes Requirement, RequirementKey), den Story 1.5 nach de/en lokalisiert (AD-13; kein deutscher Klartext im Matcher) (FR-5.1, FR-5.3)

Given ein muss/soll-Requirement ohne passenden Kandidaten (Mapping §4a) When komponiert wird Then entsteht eine benannte Lücke; bei nicht erfüllbarem Wärmebedarf wählt der Matcher die wärmste mögliche Kombination und benennt die Lücke; bei Konflikt Außenschicht vs. Wärmebedarf gewinnt die höhere Priorität (FR-5.2, §4.6)

Given ein Stück mit Temperaturbereich When gefiltert wird Then ist der Kandidat zulässig, wenn sein Bereich mit [morgen_wert, tagesmax] beidseitig inklusiv überlappt; fehlt eine Grenze → Punkt-/Halbintervall; fehlen beide Temperaturwerte → Filter inaktiv (AD-22); aktiv = nein wird nie gewählt (FR-2.2)

Given ein effektiv leerer Schrank (leer / alle inaktiv / alle ausgefiltert) When komponiert wird Then entsteht keine Ausnahme; das Ergebnis nennt die Requirements als Kategorien + Ursache (FR-2.3)

Given das normative Beispiel-Set (§8) + Frost/Regen-Fixture (1 °C, 70 % Regen) When der Abnahmetest läuft Then ergibt sich Thermoshirt + Wollpullover + Regenjacke (außen, wasserdicht) + Jeans + Stiefel + Mütze + Schal; einzige Lücke: Handschuhe (§8)

Story 1.5: Regeltext, Texte (de/en) & Payload-Projektionen

As a Nutzer, I want eine kurze, verständliche, erklärende Empfehlung in meiner Sprache und vollständig nachvollziehbare Ausgabedaten, So that ich die Empfehlung auf einen Blick verstehe und Automationen zuverlässig darauf aufbauen können.

Acceptance Criteria:

Given eine fertige Recommendation When texts.py den Text baut Then entsteht ein deterministischer Kurztext (≤ 255 Zeichen) und ein Volltext, je in de/en gemäß Sprache (Fallback en; Matching über primäres Subtag, AD-13); der Volltext webt die Stück-Gründe ein (FR-5.3) und nennt den Datenlage-Hinweis, wenn ein muss/soll-Requirement betroffen ist (FR-4.3) — nicht nur als Attribut

Given die Recommendation When die Projektionen erzeugt werden Then liefert logic/model.py eine to_payload() (englische Key-Liste aus 1.1) und die vier Projektionen to_state() (≤255), to_attributes() (≤15 KB, Stückliste ≤30 + Restzähler, Shedding-Kaskade, enthält stale, kein llm_text), to_event() (≤32 KB, Assert), to_response() (vollständig)

Given übergroße Eingaben When to_attributes() projiziert Then greift die deterministische Shedding-Kaskade (erst Stückliste, dann Kurzgründe, dann Requirement-Freitexte); Status/Zieldatum/Keys/Lücken weichen nie (AD-14, NFR-8)

Given zwei Empfehlungen mit gleichem vs. verändertem Outfit When die signature (rein, im Kern) berechnet wird Then ist sie stabil bei gleichem Inhalt und ändert sich bei verändertem Outfit/Lücken/alert; alert ⇔ mind. ein muss-Requirement jenseits base_*; changed/stale bleiben im Kern auf Default (Owner: Coordinator/Sensor) (AD-19)

Story 1.6: Wetter-Adapter (WeatherProvider-Port + HA-Entität)

As a Nutzer, I want dass die Integration die Prognose meiner Wetter-Entität robust und einheitensicher abruft, So that die Empfehlung auf verlässlichen Wetterdaten beruht.

Acceptance Criteria:

Given weather/provider.py When es geladen wird Then definiert es das Protocol WeatherProvider mit async get_raw_forecast() -> RawForecast; der spätere Coordinator typisiert nur gegen dieses Protocol (INV-6)

Given eine gewählte weather.*-Entität When weather/ha_entity.py (implementiert WeatherProvider) die Prognose abruft Then nutzt es hass.services.async_call("weather","get_forecasts", …, blocking=True, return_response=True) nach get_supported_features-Check (daily und/oder hourly; nur-twice_daily = ungeeignet), parst alle Zeitstempel mit dt_util.parse_datetime (verwirft naive/unparsebare → Feld fehlend), liest die Einheiten-Attribute, mappt Conditions ({rainy,pouring,lightning-rainy,hail}→regen; {snowy,snowy-rainy}→schnee; {lightning,lightning-rainy}→gewitter) und liefert einen TZ-awaren RawForecast (AD-17/AD-24)

Given ein phcc-Integrationstest mit einer Mock-Weather-Entität When der Adapter läuft Then ist die Suite grün (inkl. Entität ohne passenden Forecast-Typ, unavailable, Response-Key fehlt), und normalize.py (1.2) verarbeitet den gelieferten RawForecast zu einer plausiblen NormForecast

Story 1.7: Coordinator (einziger Mutator, Trigger, Delta, Timing)

As a Nutzer, I want dass die Integration die Empfehlung aktuell hält und nie stumm ausfällt, So that die Empfehlung ohne mein Zutun stimmt.

Acceptance Criteria:

Given der DataUpdateCoordinator (erzeugt mit config_entry=entry) When er aktualisiert Then ist er der einzige, der die Recommendation berechnet (asyncio.Lock um _async_update_data), mit Zieldatum-Logik (morgen ⇔ Uhrzeit ≥ Umschaltzeit, lokal/DST-fest via async_track_time_change), asyncio.timeout(10) um den Forecast-Abruf, und den Triggern stündlich / Neustart / Umschaltzeitpunkt / Quell-Entität-verfügbar (AD-3/AD-7)

Given ein Forecast-Fehler When _async_update_data läuft Then liefert es ohne Vorgänger-Daten Recommendation(status=fehler_prognose) (nie data is None); mit Vorgänger-Daten → raise UpdateFailed (letzte Empfehlung bleibt) (AD-2/AD-7, FR-3.4)

Given die zuletzt ausgelieferte Signatur (persistiert via helpers.storage.Store, pro Entry) When eine neue Empfehlung entsteht Then setzt der Coordinator changed = (signature ≠ persistierte Vor-Signatur) und pflegt die Zeitmarke last_success_utc für die Stale-Berechnung (AD-19/AD-2)

Given ein phcc-Test mit gemocktem Provider When der Coordinator ohne LLM rechnet Then ist die Suite grün (inkl. Start-mit-fehlender-Entität → status=fehler_prognose, nie None) und eine Berechnung dauert gemessen < 5 s (NFR-7-Timing-Assert)

Story 1.8: Config-Flow (Wetter-Entität + Test-Abruf) & Entry-Lebenszyklus

As a Nutzer, I want WTW über die HA-UI einzurichten, So that die Einrichtung geführt und fehlerrobust ist.

Acceptance Criteria:

Given ein frischer Config-Flow When ich eine weather.*-Entität per Selector wähle Then verifiziert der Flow sie sofort per Test-Abruf (nutzt get_supported_features/morning_window_coverage aus 1.6/1.2) und unterscheidet vier übersetzte Fehlerfälle: keine weather.*-Entität / keine Prognose / Prognose reicht nicht bis Zieldatum / temporär (unavailable/unknown/Timeout 10 s); jede Meldung nennt Ursache und einen Handlungshinweis/Doku-Link (FR-1.2/J1/C-1)

Given der Config-Flow When ich ihn durchlaufe Then kann ich den Kälteempfinden-Offset (2…+2, Default 0, Richtung im Label »Ich friere leicht ↔ Mir ist schnell warm«) setzen; genau ein Config-Entry (single_config_entry); der Flow liefert seine en/de-Übersetzungsschlüssel inkrementell mit (FR-1.3/FR-1.6/AD-13)

Given ein angelegter Entry When async_setup_entry / async_unload_entry / async_remove_entry laufen Then richtet Setup den Coordinator + die Listener ein und cancelt beim Unload alle Listener (Zeit-, Entity-, Options-Listener) leak-frei via entry.async_on_unload; ein phcc-Test weist Setup→Unload→Setup ohne Doppel-Registrierung nach — mit PLATFORMS=[] (Coordinator/Listener-Lebenszyklus, ohne Sensor-Plattform-Forwarding; das schaltet Story 1.9 scharf, keine Vorwärts-Abhängigkeit) (AD-7/AD-10)

Given phcc-Integrationstests When sie laufen Then decken sie alle vier Fehlerfälle und den Setup/Unload-Zyklus grün ab (NFR-3)

Story 1.9: Sensor mit stabiler ID & Stale-Kennzeichnung

As a Nutzer, I want sofort eine sichtbare Empfehlung auf einem stabil benannten Sensor, So that sich die Einrichtung sofort sichtbar lohnt.

Acceptance Criteria:

Given ein Entry mit laufendem Coordinator When die Sensor-Plattform lädt Then existiert sensor.what_to_wear (stabile ID: _attr_unique_id + self.entity_id vor async_add_entities, has_entity_name + translation_key) mit State = to_state() und Attributen = to_attributes(); __init__ forwardet ab dieser Story Platform.SENSOR (aktiviert die in 1.8 vorbereitete Setup/Unload-Mechanik) (FR-7.1/AD-15)

Given die letzte erfolgreiche Berechnung ist > 6 h her (last_success_utc) When der Sensor benachrichtigt wird Then berechnet der Sensor (Stale-Besitz, AD-2) stale = true, schreibt es in die Attribute und plant einen Einmal-Timer auf last_success + 6 h für den exakten Kipp-Zeitpunkt (FR-7.5)

Given ein Entry ohne gepflegte Stücke When der Sensor rechnet Then zeigt er die »leerer Schrank«-Empfehlung (Kategorien + Ursache), nie einen Absturz (FR-2.3); phcc-Test deckt Attribute + Stale + leerer-Schrank grün ab


Epic 2: Der 15-Minuten-Wow (Kleiderschrank → Karte → Push)

Ziel: der vollständige S-2-Pfad. Baut auf Epic 1 (Matcher/Sensor/Coordinator/Payload). Nach 2.5 kann ein neuer Nutzer in < 15 min installieren, Beispiel-Set anlegen, das Outfit auf der Karte sehen und den Push aufs Handy einrichten. Jede Story ist eigenständig testbar.

Story 2.1: Kleidungsstücke anlegen, bearbeiten, löschen (Sub-Entries)

As a Nutzer, I want meine Kleidungsstücke direkt in der HA-UI zu pflegen, So that die Empfehlung meinen echten Schrank nutzt.

Acceptance Criteria:

Given ein bestehender Config-Entry When ich ein Stück anlege Then führt ItemSubentryFlow.async_step_user durch die 3 Pflichtfelder (Name 160 Zeichen; Kategorie aus dem Category-Enum via Selector; Wärmegrad 15) und die optionalen Felder (wasserdicht, winddicht, Sonnenschutz, Formalität leger/business, Temperaturbereich min ≤ max, aktiv) mit Validierung (FR-2.1/AD-22); Daten im englischen Schema (AD-22); Stück-ID = subentry_id

Given ein vorhandenes Stück When ich es bearbeite Then öffnet async_step_reconfigure (inkl. editierbarem Titel) den Flow mit aktuellen Werten; Zugriff über den Kompat-Accessor (2025.3 _get_reconfigure_entry / 2025.4+ _get_entry) (AD-9)

Given ein Stück angelegt/geändert/gelöscht When der Flow abschließt Then feuert die Änderung den Update-Listener bzw. der Flow stößt selbst async_request_refresh an; die nächste Empfehlung nutzt den neuen Stand (AD-7, J4); der Flow liefert seine en/de-Keys mit

Given phcc-Integrationstests When sie laufen Then decken sie Anlegen, Reconfigure und Löschen grün ab, inkl. Recompute nach Änderung (J4: neues Stück in < 1 min → nächste Empfehlung nutzt es)

Story 2.2: Beispiel-Kleiderschrank beim Setup

As a neuer Nutzer, I want beim Einrichten optional ein Beispiel-Set anlegen zu lassen, So that ich ohne eigene Pflege sofort eine vollständige Empfehlung sehe.

Acceptance Criteria:

Given der initiale Config-Flow (nur dort, nie im Reconfigure/Options) When ich »Beispiel-Kleiderschrank anlegen« wähle Then hängt der Flow die 12 Stücke aus §8 additiv als subentries=-Parameter an die bestehende async_create_entry (Story 1.8) an; alle sind sofort editier-/löschbar (FR-1.5/AD-9)

Given die Namen der Beispiel-Stücke When sie angelegt werden Then stammen sie aus logic/texts.py in der Setup-Sprache (de/en), nicht aus translations/ (AD-13); Kategorien = Category-Enum-Werte

Given das angelegte Beispiel-Set + Frost/Regen-Fixture When der Abnahmetest läuft Then ergibt sich ein vollständiges Outfit mit einziger Lücke »Handschuhe« (FR-1.5, §8); der Reconfigure-Pfad bietet die Option nicht erneut an (keine Duplikate)

Story 2.3: Read-only Lovelace-Karte mit Auto-Registrierung

As a Nutzer, I want eine Dashboard-Karte, die meine Empfehlung dauerhaft und ehrlich zeigt, So that ich abends und morgens auf einen Blick sehe, was rauszulegen ist — und ob die Daten frisch sind.

Acceptance Criteria:

Given die Integration lädt (Storage-Mode) When das Setup läuft Then registriert frontend.py die statische Datei via async_register_static_paths([StaticPathConfig("/what_to_wear/what-to-wear-card.js", …)]) und legt/aktualisiert die Lovelace-Ressource idempotent (URL-Prefix-Scan, async_update_item auf ?v=<version>, sonst async_create_item) — nur über Attribut-Zugriff hass.data["lovelace"].resources; YAML-Mode: nicht schreiben, README-Schritt; async_remove_entry deregistriert (AD-12/FR-7.3)

Given die Karte rendert Empfehlungsdaten When Stücknamen/Texte/Gründe/Lücken angezeigt werden Then nutzt sie ausschließlich textContent/DOM-APIs (nie innerHTML mit Daten — XSS-sicher), zeigt Empfehlung/Zieldatum/Stücke/Lücken und rendert nur gelieferte Strings (AD-12/AD-13)

Given die Attribute tragen stale = true bzw. tone = rules mit data_notes-Grund When die Karte rendert Then zeigt sie einen sichtbaren »veraltet«-Hinweis (FR-7.5) und einen dezenten Ton-Indikator (LLM aktiv/Regeltext), damit ein dauerhafter LLM-Fallback am Dashboard sichtbar ist; bei fehlender/unavailable Entität einen Platzhalter (nie Exception)

Given phcc-Test der Registrierung + Review/Lint der JS-Datei When sie laufen Then ist die Registrierung idempotent grün und die JS-Datei enthält nachweislich keinen innerHTML-Datenpfad (verankertes Kriterium für das deklarative JS-Artefakt)

Story 2.4: Service recommend mit garantiert frischer Response + Event

As a Automatisierer, I want einen Service, der eine frische Empfehlung erzwingt und zurückgibt, So that meine Automationen immer den aktuellen Stand ausspielen.

Acceptance Criteria:

Given der registrierte Service what_to_wear.recommend (SupportsResponse.OPTIONAL) When er aufgerufen wird Then ruft der Handler await coordinator.async_refresh() (ungedrosselt; Coordinator serialisiert über sein Lock), liest danach coordinator.data und liefert to_response() nur bei call.return_response; er feuert immer das Event what_to_wear_recommendation mit to_event() (AD-11/AD-19)

Given zwei nahezu gleichzeitige Aufrufe plus ein parallel laufender Zeit-Trigger When sie eintreffen Then stammt jede Response aus einer nach Call-Eingang gestarteten/abgeschlossenen Berechnung (Frische-Garantie; phcc-Test mit Provider-Zähler inkl. parallelem Trigger)

Given services.yaml When es geladen wird Then definiert es nur Struktur/Selectors; Name/Beschreibung kommen aus translations (services.recommend.*, inkrementell mitgeliefert)

Story 2.5: Blueprints Push & TTS

As a Nutzer, I want fertige Automationsvorlagen für Handy-Push und Sprachansage, So that ich ohne YAML-Bastelei benachrichtigt werde.

Acceptance Criteria:

Given die zwei Blueprints (blueprints/automation/what_to_wear/notify_push.yaml, announce_tts.yaml) When ich sie über den README-Import-Link (my.home-assistant.io/redirect/blueprint_import/?blueprint_url=…) importiere Then haben beide min_version: 2025.3.0; Push nutzt Device-Selector (Filter mobile_app) + time-Selector; TTS media_player-entity-Selector + Auslöser (FR-7.4)

Given eine Blueprint-Automation When sie auslöst Then ruft sie zuerst what_to_wear.recommend mit response_variable auf und verwendet ausschließlich dessen Rückgabe (response.llm_text | default(response.full_text)), nie den Event (AD-11/AD-20)

Given die Push-Option »nur senden, wenn geändert oder Lücke/Warnlage« When sie aktiv ist Then entscheidet die Automation anhand der Response-Felder changed, gaps_count > 0 bzw. alert (AD-19) — deckt J2 (Push mit Lücke »Handschuhe«) und J3 (nur re-notify bei Änderung) ab

Given ein YAML-Lint/Schema-Check der Blueprints When er läuft Then sind beide gültig (blueprint.domain: automation, input: mit Selectors) — verankertes, nachprüfbares Akzeptanzkriterium (deklaratives Artefakt)


Epic 3: Anpassung & Robustheit

Ziel: Konfigurierbarkeit + verlustfreie Migration. 3.1 erzeugt den Options-Flow (Scaffold), den Epic 4 (LLM) additiv erweitert. Baut auf Epic 1 (const.py-Schema, rules liest Schwellen, Test-Abruf).

Story 3.1: Options-Flow (Schwellen, Umschaltzeit, Wetter-Entität)

As a Nutzer, I want Schwellwerte, Umschaltzeit und Wetter-Entität nachträglich anzupassen, So that ich WTW an mein Klima und meinen Tagesrhythmus anpassen kann.

Acceptance Criteria:

Given der Options-Flow (neue OptionsFlow-Klasse, Property-Muster ohne __init__, AD-10) When ich ihn öffne Then kann ich jede Schwelle einzeln setzen (Wertebereich + Validierung; Bandgrenzen als strikt monotone Liste), den Umschaltzeitpunkt (TimeSelector HH:MM:SS), den Offset und den Wetter-Entität-Wechsel (mit Test-Abruf wie FR-1.2, Reuse aus 1.6/1.8) (FR-1.4)

Given das Options-Schema When Werte gespeichert werden Then liegen sie flach/englisch nach const.py-Schema (AD-23); der Handler schreibt die gewechselte Wetter-Entität nach entry.data (AD-8); die Umschaltzeit wird an genau einer Stelle mit time.fromisoformat geparst

Given eine geänderte Option When der Options-Flow abschließt Then triggert der Update-Listener async_reload (Zeit-Listener neu, leak-frei via 1.8-Lebenszyklus); Optionen wirken sofort auf rules.py (das die Schwellen bereits aus dem Schema liest, 1.3) (AD-7/AD-10)

Given phcc-Tests When sie laufen Then decken sie Options-Roundtrip, monotone Validierung, HH:MM:SS-Parsing und Entität-Wechsel grün ab

Story 3.2: Schema-Versionierung & Migration

As a Nutzer, I want dass Updates meine gepflegten Stücke und Einstellungen nie verlieren, So that ich WTW bedenkenlos aktualisieren kann.

Acceptance Criteria:

Given ein Config-Entry mit VERSION=1, MINOR_VERSION=1 When die Integration lädt Then existiert async_migrate_entry; ein v1→v1-Lauf ist ein No-op ohne Datenverlust (NFR-10/AD-8)

Given fehlende, unbekannte oder typfalsche Options-/Subentry-Keys When gelesen wird Then greift der Robustheits-Contract: fehlend ⇒ Default aus const.py, unbekannt ⇒ bleibt erhalten, typfalsch ⇒ Default + Warn-Log — nie Setup-Abbruch (AD-8)

Given phcc-Tests When sie laufen Then decken sie den No-op-Migrationslauf und den Robustheits-Contract grün ab (FR-2.4-Verhalten »Löschen entfernt Subentries« ist Core-Verhalten; die README-Warnung liefert Story 5.3)


Epic 4: Optionaler LLM-Ton (Bring-your-own-Key)

Ziel: schönerer Ton per opt-in, sicher, mit garantiertem Fallback. Erweitert den Options-Flow aus 3.1 additiv. Sicherheitskritisch → stärkstes Modell bei der Umsetzung.

Story 4.1: LLM-Optionen & Datenschutz-Disclosure

As a Nutzer, I want den LLM-Ton mit meinem eigenen Key transparent zu aktivieren, So that ich die Kontrolle über Kosten und übertragene Daten behalte.

Acceptance Criteria:

Given der bestehende Options-Flow (3.1) When ich den LLM-Ton aktiviere Then ergänzt eine additive Options-Sektion Provider (openai/anthropic), Key (Passwort-Selector, maskiert) und optional Modell; Default aus (FR-6.1/AD-23)

Given der Aktivierungs-Step When er angezeigt wird Then zeigt er vor dem Speichern die Datenübertragungs-Disclosure (Stücknamen, Wetterkennwerte, Zielsprache; Drittland möglich; Link auf Provider-Bedingungen) als Step-Description (FR-6.5)

Given ein bereits gespeicherter Key When ich den Options-Flow erneut öffne und nur anderes ändere Then wird der Key nie als Default/suggested_value zurückgespielt; leeres Feld ⇒ Key bleibt; Entfernen nur über »LLM deaktivieren« (AD-5); phcc-Test deckt Aktivieren, Disclosure, »leer lassen« grün ab

Story 4.2: LLM-Phraser & Injection-Katalog (reiner Kern)

As a Nutzer, I want einen sprachlich schöneren, aber inhaltlich unveränderten und injektionssicheren Empfehlungstext, So that die Ansage angenehm klingt, ohne Fakten, Auswahl oder Sicherheit zu gefährden.

Acceptance Criteria:

Given eine strukturierte Empfehlung When logic/phraser.py den Prompt baut Then übergibt es Stücknamen JSON-escaped in einem JSON-Block (Untrusted Data) + Ignorier-Instruktion; nie Fotos/Koordinaten/Entity-IDs (FR-6.2/AD-16)

Given eine LLM-Antwort (String, aus dem Adapter übergeben) When phraser.py sie validiert Then prüft es (a) reiner Fließtext ohne </[/```/http(s)://, (b) ≤ 700 Zeichen, (c) jeder empfohlene Stückname wörtlich enthalten, (d) jede Lücke genannt, (e) nach Trim ≥ 20 Zeichen; jede Verletzung ⇒ None (→ Adapter/Coordinator nutzt Regeltext) (FR-6.6)

Given der Injection-Testkatalog (bösartige Stücknamen: Markup, Links, Prompt-Befehle, 700-Zeichen-Namen) When die Unit-Tests laufen (rein, ohne HA) Then greift in jedem Fall die Ablehnung und kein Markup/Link passiert die Validierung (FR-6.6/NFR-3)

Story 4.3: LLM-Client (Adapter) & Ton-Fallback

As a Nutzer, I want dass der LLM-Aufruf sicher und mit hartem Budget erfolgt und bei jedem Fehler nahtlos auf den Regeltext zurückfällt, So that die Empfehlung nie ausfällt oder Geld/Zeit verschwendet.

Acceptance Criteria:

Given llm/client.py When es den Provider aufruft Then nutzt es async_get_clientsession(hass), URL-Konstanten aus const.py (OpenAI /v1/chat/completions Bearer; Anthropic /v1/messages mit x-api-key + anthropic-version: 2023-06-01 + max_tokens), allow_redirects=False, Timeout 18 s, Body-Cap 64 KB, genau ein Versuch; jeder Nicht-2xx/Timeout ⇒ None (AD-6)

Given die Coordinator-Verdrahtung When LLM aktiv ist und phraser.validate einen Text akzeptiert Then wird er als llm_text/tone=llm geführt; bei aus/Fehler/Timeout/ungültig/Validierungsbruch → deterministischer Regeltext, tone=rules + kategorialer Grund (llm_auth|llm_timeout|llm_invalid) in data_notes (FR-6.3/AD-16); NFR-7 (< 30 s) durch das 18-s-Budget gedeckt

Given phcc-Tests mit gemockten Provider-Antworten (2xx gültig, 2xx ungültig, 401, Timeout, Übergroß) When sie laufen Then ist die Suite grün und in jedem Negativfall erscheint der Regeltext mit korrektem tone/Grund

Story 4.4: Diagnostics-Redaction (Whitelist)

As a Nutzer, I want dass Diagnose-Downloads keine Geheimnisse oder Standortdaten enthalten, So that ich sie gefahrlos an ein GitHub-Issue anhängen kann.

Acceptance Criteria:

Given diagnostics.py When ein Diagnose-Download erzeugt wird Then ist er eine Whitelist (Options mit TO_REDACT = {llm_api_key, weather_entity_id} redigiert, Norm-Kennwerte, Status, Zieldatum, data_notes, Stück-Anzahl je Kategorie — nie Namen); kein Feld trägt die Wetter-Entity-ID (auch nicht source) (AD-5/FR-6.4/NFR-6)

Given ein voll befüllter Entry (Key + Entität + Stücke) When der Substring-Test über den Diagnostics-JSON läuft Then taucht weder der Key-Wert noch die Entity-ID im JSON auf


Epic 5: Internationalisierung & Release-Reife

Ziel: Zweisprachigkeit-Vollständigkeit, CI, HACS-/Brands-/Lizenz-Konformität, README, E2E — HACS-ready. Baut auf allen vorigen Epics (die en/de-Keys wurden inkrementell mitgeliefert; hier Vollständigkeits-/Konformitäts-Validierung).

Story 5.1: Vollständige Internationalisierung (de/en)

As a Nutzer, I want die gesamte UI und alle Empfehlungstexte in meiner Sprache, So that WTW sich in meiner Sprache natürlich anfühlt.

Acceptance Criteria:

Given die inkrementell gelieferten translations/en.json und de.json When die Konsolidierung läuft Then sind alle registrierten Texte vollständig (config/options/config_subentries.item.initiate_flow.{user,reconfigure}/entity/services/exceptions/selector); en ist vollständig; kein strings.json (AD-13)

Given hassfest When die CI läuft Then validiert es die Übersetzungen ohne Fehler (keine eigenen Top-Level-Keys) (AD-13/NFR-5)

Given die dynamischen Empfehlungstexte When eine Berechnung läuft Then folgen sie hass.config.language je Berechnung (Fallback en); registrierte Texte nutzen die HA-Translations; die Entity-ID bleibt sprachneutral fix (FR-1.3/FR-8.1)

Story 5.2: CI-Pipeline, HACS-/Brands-/Lizenz-Konformität

As a Maintainer, I want eine grüne CI und HACS-konforme Metadaten, So that WTW verlässlich installierbar und HACS-ready ist.

Acceptance Criteria:

Given .github/workflows/ci.yaml When die CI läuft Then enthält sie eine Matrix (Job »min«: Python 3.13 + phcc 0.13.225 = HA 2025.3.4; Job »latest«: Python 3.14 + aktuelles phcc), führt pytest, hassfest (home-assistant/actions/hassfest) und die HACS-Action (category: integration) aus; permissions: contents: read als Default; ein Ressourcen-Registrierungs-Test läuft im latest-Job (AD-10/AD-18/NFR-4). Das Grün-Kriterium bezieht sich auf den CI-Runner selbst und ist unabhängig vom GitHub-Mirror-Setup (E-3)

Given die Metadaten When sie geprüft werden Then sind hacs.json (name, homeassistant: "2025.3.0"), manifest.json (alle Pflichtfelder) vollständig; LICENSE (Apache-2.0) und NOTICE liegen im Repo; keine gebündelten Fremdlizenzen (Karte ohne Libs) (NFR-5/NFR-9)

Given der brands-Eintrag When er vorbereitet wird Then ist ein PR an home-assistant/brands (custom_integrations/what_to_wear/icon.png 256×256 + icon@2x.png 512×512) vorbereitet; zusätzlich custom_components/what_to_wear/brand/ mitgeliefert (wirkt ab 2026.3) (NFR-5) — die Einreichung selbst ist ein Benutzer-Schritt (Story 5.3)

Story 5.3: README, E2E-Verifikation & Release-Freigabe

As a Nutzer, I want eine klare Doku und den Nachweis, dass WTW real und schnell funktioniert, So that ich WTW vertrauensvoll installiere und einrichte.

Acceptance Criteria:

Given README.md When es erstellt ist Then enthält es die Pflichtabschnitte: Datenschutz de+en (FR-6.5) inkl. at-rest/Backup-Hinweis (FR-6.4) und LLM-Rest-Risiko (AD-16), Inventar-Löschwarnung (FR-2.4), YAML-Mode-Schritt (AD-12), Blueprint-Import-Badges (FR-7.4), Setup-Doku-Links + Custom-Repository-Installationsweg

Given eine Wegwerf-HA-Docker-Instanz When die E2E-Verifikation läuft Then wird der Kern-Flow real und getaktet durchgespielt: Installation → Wetter-Entität → Beispiel-Set → Karte sichtbar → Blueprint-Push real; die erste Empfehlung entsteht in < 15 min (S-2, Protokoll unter docs/beta/); ein phcc-Assert weist nach, dass WTW beim Setup keine Blocking-/Startzeit-Warnung erzeugt (C-2/NFR-2) und im Default-Setup (LLM aus) keine ausgehende Netzverbindung öffnet (NFR-1, z. B. via gepatchter Session/aioclient_mock ohne erwartete Calls); Negativfälle (keine Wetter-Entität, Entität ohne Forecast, falscher LLM-Key)

Given CHANGELOG.md, Version v1.0.0 (== manifest.json) und der GitHub-Mirror/Release When das Release vorbereitet wird Then werden die irreversiblen/identitätskritischen Schritte dem Benutzer vorgelegt (GitHub-Mirror-Einrichtung E-3 mit Leitplanken, brands-PR-Einreichung, HACS-Default-Antrag, Tag/Release) und nicht autonom ausgeführt; »CI grün auf dem Mirror« ist das finale, messbare Release-Gate nach der Mirror-Freigabe (S-3)