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>
48 KiB
| stepsCompleted | inputDocuments | status | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
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 1–5) + optionale Felder (wasserdicht, winddicht, Sonnenschutz, Formalität, Temperaturbereich, aktiv).
- FR-2.2 Stücke mit
aktiv = neinwerden 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.recommenderzwingt 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
recommendvorher 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 keinhomeassistant.*(auch keindt_util); Zeit-Parsing im Adapter (weather/ha_entity.py), Kern nutztzoneinfo. 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, niedata 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
recommendOPTIONAL-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 inlogic/texts.py; Karte rendert nur gelieferte Strings. - AD-14/AD-19/AD-20 Frozen
Recommendation; eineto_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_idvorasync_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_forecastsblocking+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 inrules.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.1–4.4 → Story 1.3 (Regel-Engine); Konfigurierbarkeit: Schema 1.1 + Wiring 3.1
- FR-5.1–5.3 → Story 1.4 (Matcher) + Story 1.5 (Gründe im Text)
- FR-6.1–6.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.1–3.4, FR-4.1–4.4, FR-5.1–5.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.1–6.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.1–1.5 bauen den hass-freien Kern (rein, mit Fixtures testbar), 1.6–1.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 1–60 Zeichen; Kategorie aus dem Category-Enum via Selector; Wärmegrad 1–5) 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)