Stand: 2026-08-07 — gegen den Baum gemessen, nicht fortgeschrieben.
Nachgemessen am 2026-08-11: die Modul-Liste unter core/berechnungen/ (36 Module, maschinell gegen das Verzeichnis geprüft) und die services/-Neuzugänge des Wirtschaftlichkeits- und Cloud-Import-Pakets. Die übrigen Kapitel tragen weiterhin den Stand vom 07.08.
Dieses Dokument trägt bewusst keine Versionsnummer. Der Versions-SoT ist
CHANGELOG.md (oberster released Abschnitt) bzw.
eedc/backend/core/config.py::APP_VERSION; scripts/release.sh bumpt docs/nicht. Eine
Zahl an dieser Stelle veraltet daher garantiert — genau daran ist der vorige Kopf gescheitert
(er stand bis 2026-08-07 auf „v3.24.1, April 2026”, während v4.0.10 ausgeliefert war).
Wer die Oberfläche ändert, findet die verbindlichen Regeln nicht hier, sondern in den drei
SoT-Regimen — siehe Die drei SoT-Regime weiter unten. Dieses Dokument
beschreibt Einrichtung, Struktur und Werkzeuge, keine Fachregeln.
eedc-homeassistant ist die Source of Truth. Alle Änderungen (Backend, Frontend, Docs, HA-Config) hier machen. Das eedc-Standalone-Repo ist ein Spiegel und wird per Release-Script synchronisiert.
Ein Release-Script bumpt alle Versionsdateien, committed, taggt, pusht, synchronisiert das
Standalone-Repo — und wartet zum Schluss, bis das Add-on-Image wirklich in der Registry liegt:
Terminal-Fenster
./scripts/release.sh<version># z. B. die nächste Patch-Nummer laut CHANGELOG
Datei
Feld
eedc/backend/core/config.py
APP_VERSION
eedc/frontend/src/config/version.ts
APP_VERSION
eedc/config.yaml
version (HA Add-on)
eedc/run.sh
Startbanner
eedc/Dockerfile
io.hass.version-Label
CHANGELOG.md
neuer Abschnitt (manuell vor dem Release; wird nach eedc/ kopiert)
HA-Nutzer erreicht nur ein Release. Jede Änderung, die bei ihnen ankommen soll, braucht eine
neue Version — ein Commit auf main genügt nicht.
⚠ Ein grüner Push ist kein ausgeliefertes Add-on.eedc/config.yaml zieht ein vorgebautes
Image von ghcr.io, das ein Workflow nach dem Tag baut. Zwischen Tag und fertigem Image
zeigt der Store die neue Version, während die Installation [404] manifest unknown meldet — am
2026-08-06 klaffte dieses Fenster wegen einer GitHub-Actions-Störung sechs Stunden, und drei
Anwender sind hineingelaufen. Der Beleg einer Auslieferung ist das Image-Manifest, nicht der
grüne Push:
Terminal-Fenster
REPO=supernova1963/eedc-homeassistant-amd64# bzw. …-aarch64
Beide Header sind Pflicht. Ohne Authorization antwortet die Registry auf jede Version mit
401, ohne den Accept-Header auf jede mit 404 — ein Prüfer, der das vergisst, meldet ein
längst ausgeliefertes Image als fehlend. Gemessen am 2026-08-07: 4.0.10 → 200, 4.0.9 →
200, erfundene 4.0.99 → 404. Immer gegen die Vorversion und eine erfundene Version
gegenprüfen, bevor ein Alarm daraus wird.
Von Hand braucht man das nur noch zur Diagnose.release.sh ruft als Schritt 7
scripts/warte-auf-image.sh <version> auf, das genau diesen Abruf für beide Architekturen
wiederholt (alle 20 s, längstens 40 Minuten) und erst danach „ausgeliefert” meldet. Das Script
weist sich vorher an der Vorversion aus und bricht mit einer eigenen Meldung ab, wenn schon
die als fehlend gemeldet wird — dann ist der Prüfer kaputt, nicht das Release. Exit-Codes:
0 ausgeliefert · 1 Wartezeit abgelaufen, Image fehlt wirklich · 2 Prüfer nicht
vertrauenswürdig · 130 per Ctrl-C abgebrochen. Nach einer Störung (Actions-Ausfall,
abgebrochener Build) startet man es allein, ohne das Release anzufassen:
Type Hints verwenden; Docstrings für öffentliche Funktionen — und zwar mit dem Warum, nicht
nur dem Was. Die Docstrings sind hier Trägermedium der Regeln (etwa „drei gleichwertige
Transporte, eine Quelle” in ha_statistics_service.py).
black und ruff stehen in requirements.txtauskommentiert und sind kein Gate. Wer
formatiert, formatiert von Hand im Stil der Umgebung — kein Repo-weiter Reformat-Lauf.
Neuer Code liest im Stil der Nachbarschaft: gleiche Kommentar-Dichte, gleiche Benennung.
npx tsc --noEmit ist das Gate, npm run lint (ESLint, --max-warnings 0) die Ergänzung.
Funktionskomponenten mit Hooks; abgeleitete Logik in reine, testbare Funktionen ziehen
(Beispiel waehleDefaultMonat) — eine reine Funktion lässt sich rot verifizieren, ein
useEffect nicht.
Keine Inline-Hex-Farben außerhalb lib/colors.ts, keine Roh-Controls, keine lokalen
Label-/Wochentags-Arrays — dafür gibt es Wächter, und sie fangen es.
Betreffs sind deutsch und sagen die Wirkung, nicht die Tätigkeit — typ(bereich): aussage:
fix(boersenpreis): ein Tag ist der Tag der Marktzone, nicht der UTC-Tag
feat(cockpit-tag): der Speicher-Block nennt auch den Ladezustand
docs: Doku-Durchgang vor <version>
release: v<version>
Verwendete Typen: fix · feat · docs · refactor · perf · build · release. Der Body
nennt Ursache, Wirkung und Melder (Issue/Forum/PN) — er ist die einzige Stelle, an der später
noch steht, warum etwas so ist.
⚠ Nur explizite Pfade committen, nie git add -A — es laufen parallele Sessions im selben
Arbeitsbaum. git push, Tags und Versionsnummern passieren ausschließlich auf ausdrückliche
Anweisung bzw. über scripts/release.sh.
Invarianten P1–P10 — was ein Wert behaupten darf und woher er kommen muss
backend/tests/test_wurzelmuster_*.py
Dazu die Oberflächen-Invarianten I1–I16 in KONZEPT-IA-V4.md (IA, Park-Doktrin,
Redirect-Tabelle) und die Monatszeilen-Schicht in
KONZEPT-MONATS-FAKTEN.md (ADR-002/P10).
Backend-Wächter sind pytest, keine check:*-Skripte. Alle check:* sind Node-Skripte im
Frontend. Zwei Ausnahmen bewachen die Client-Hälfte einer Backend-Regel: check:kennwert-roh
(ADR-002/P3-a) und check:co2-roh (ADR-001/DI-2).
Regel 0a in einem Satz: Wer etwas Sichtbares baut und dafür eine Regel/SoT vorfindet, wendet
sie an; existiert keine, aber wäre sinnvoll, wird sie in derselben Arbeit definiert und die
Zentrale erweitert; ein echter Einzelfall braucht Maintainer-Freigabe, Code-Kommentar und einen
Eintrag in der Ausnahmen-Liste.
Diese Liste ist der Einstieg. Die vollständige Fassung samt Fallstricken steht in
CLAUDE.md §„Kritische Code-Patterns” und §„Bekannte Fallstricke” — dort wird sie
gepflegt, hier stehen die vier, die am häufigsten getroffen werden.
Monatswerte nur aus den Monats-Fakten (ADR-002/P10)
SoT ist eedc/backend/services/monats_fakten.py. Wer eine abgeleitete Monatsgröße auswertet,
faltet InvestitionMonatsdatennicht selbst — Zeitfilter, Dienstwagen-Filter und Auflösung
sind dort schon drin:
from backend.services.monats_fakten import lade_monats_fakten, finanz_zeile_eingabe
Die Nennleistung liegt je nach Herkunft in der SpalteInvestition.leistung_kwpoder im
parameter-JSON — die Spalte allein zu lesen liefert bei Import-/Altbestand still 0:
from backend.core.investition_kennwerte import get_erzeuger_kwp, get_pv_kwp, get_bkw_kwp
kwp =get_erzeuger_kwp(inv) # RICHTIG — Typ-Dispatcher (BKW vs. PV-Modul)
kwp = inv.leistung_kwp # FALSCH — auch als getattr() gewächtert
leistung_kwp ist ein Mehrzweckfeld: beim Speicher trägt dieselbe Spalte kWh, beim
Wechselrichter kW (AC). Die Helper gelten nur für Erzeuger-Typen, der Aufrufer filtert.
Vollständig gegen den Baum erhoben (2026-08-07). Wer eine Datei hinzufügt oder löscht, zieht
diese Liste mit — sie ist der einzige Ort, an dem der Bestand am Stück steht. Die Zahlen in den
Kommentaren sind Stichtagswerte; bei Abweichung gewinnt der Baum.
eedc-homeassistant/ ← Source of Truth (alle Änderungen hier)
├── README.md # Projekt-Übersicht
├── CHANGELOG.md # Versions-SoT (Master, hier editieren)
Schema: Wird beim ersten Start automatisch erstellt
Für Schema-Änderungen:
Model in backend/models/ anpassen
Die Spalte in core/database.py::run_migrations eintragen (dort steht je Tabelle eine Liste,
aus der beim Start ein ALTER TABLE … ADD COLUMN erzeugt wird, wenn die Spalte fehlt)
Backend neu starten
Nicht auf SQLAlchemy verlassen:create_all legt fehlende Tabellen an, aber keine fehlenden
Spalten in bestehenden Tabellen. Genau dafür gibt es run_migrations — wer die Spalte dort
vergisst, bekommt sie auf einer frischen Installation und nicht auf einer bestehenden.
Die parameter-JSON-Spalte in investitionen wird automatisch erweitert (kein ALTER TABLE
nötig) — neue Schlüssel gehören aber in
core/investition_parameter.py + lib/investitionParameter.ts, sonst driften Backend und Client
auseinander.
Daten-Migrationen (nicht Schema, sondern Inhalt) liegen in services/migrations/ und werden
aus core/database.py::_run_data_migrationseinmalig gefahren. Regeln dafür:
Nie blockierend, nie über HTTP — der Start darf daran nicht hängen.
Kein „großer Heiler-Knopf”: eine Migration korrigiert eine benannte, belegte Fehlform.
Alles andere gehört in den Daten-Checker und in die Reparatur-Werkbank, wo der Nutzer punktuell
entscheidet.
Ein reiner Diagnose-Befund wird gemeldet, nicht geheilt.
Vor jedem Commit-Paket vollständig laufen lassen. Die Soll-Zahlen (pytest/Vitest) stehen bewusst
nicht hier — sie ändern sich mit jedem Paket. Wer wissen will, ob ein Lauf vollständig war, liest
die Summenzeile, nicht die letzte grüne Zeile.
Reihenfolge ist nicht beliebig:tsc --noEmit kommt vor dem ersten vollen Vitest-Lauf. Ein
vergessener Import fällt dort in Sekunden auf, im Vitest-Lauf erst nach Minuten und mit
irreführender Fehlermeldung.
# 3. Die Wächter (alle auflisten statt eine Zahl zu glauben)
npmrun2>&1|grep'check:'
Die Wächter (npm run check:*, eedc/frontend/scripts/check-*.mjs) setzen die
Darstellungs-Regeln maschinell durch — Regel 0/0a aus
KONZEPT-STYLE-GUIDE.md. Sie sind statisch (Quelltext-Analyse, kein
Browser), mit drei Ausnahmen und einer Baseline:
Wächter
Besonderheit
check:charts · check:achsen · check:chart-audit
Bei Chart-Arbeit zusätzlich Pflicht.check:chart-audit fährt Chromium gegen einen laufenden Dev-Server — jsdom rendert keine Charts, ein grüner Vitest-Lauf sagt über ein Chart nichts aus
check:park-leertest
Playwright-Livetest gegen eine laufende Box, verlangt ein VITE_DEMO_DEFAULT=true-Build. Läuft am Auslöser, nicht am Takt (Entscheid 23.08.): nur wenn eine im Paket geänderte Datei unter eedc/frontend/srcParkbar, data-park-id oder FokusKachel enthält (Basis HEAD, nicht origin/main) — und vor jedem Release. Mit 188 s der teuerste Einzelprüfer; was er als Einziger fängt, steht in CLAUDE.md §Gates. ⛔ Hier stand bis 23.08. die Anweisung, danach zwingend git checkout -- eedc/frontend/dist/ zu fahren, weil dist/ versioniert sei — das gilt seit N-246 / v4.0.15 nicht mehr: eedc/frontend/dist ist nicht versioniert (beide Dockerfiles bauen das Frontend in einer eigenen Stage), der Schutz sitzt in release.sh::pruefe_nichts_uebrig. CLAUDE.md trug den Widerruf, diese Tabelle nicht
check:form-controls
meldet „1 offen (WelcomeStep.tsx)” als dokumentierte Baseline (rc=0)
check:de-de
Scope über einen Import-Graph, nicht über Verzeichnisse: src/v4/ + src/components/ als Startknoten, dazu die transitive Hülle der von dort erreichten pages/- und config/-Dateien. ⚠ Wer den Graph anfasst, prüft beide Kanten-Formen: from '…'undlazy(() => import('…')). Bis 13.08. fehlte die dynamische Form — die sieben Einstellungs-Wizards hingen genau daran und lagen samt 15 roher Anzeigen außerhalb; die zehn pages/*Teile.tsx fielen an der config/-Kante heraus. Ein Wächter, dessen Reichweite an der Import-Form hängt statt an der Sichtbarkeit, prüft die falsche Menge. Der Rest-Zähler („N Treffer außerhalb”) ist keine Schuldenzahl — er enthält auch Nicht-Anzeigen wie URL-Parameter in src/api/
Einen baumweiten Wächter schreiben: quellbaum statt eigener rglob
Zehn Prüfer laufen über den Backend-Quelltext. Die Frage „welche Dateien gehören dazu” steht
einmal — in backend/tests/quellbaum.py:
from backend.tests.quellbaum import produktivbaum # alles ohne tests/, venv/, __pycache__
from backend.tests.quellbaum import probenbaum # der Testbaum
for datei inproduktivbaum():
datei.rel # "services/monats_fakten.py" — der Name, den der Prüfer meldet
datei.quelle # Quelltext
datei.baum # fertiger ast.Module — NICHT selbst parsen
⚠ Nicht selbst rglob + ast.parse schreiben. Bis zum 2026-08-24 tat das jeder Prüfer für
sich — neun handgeschriebene Kopien derselben Regel in vier Schreibweisen, und zwei davon waren
falsch: test_n252_speicher_wirkungsgrad_deckung.py filterte mit "/venv/" in rel auf einem
relativen Pfad und nahm deshalb 3828 Dateien statt 337 (3491 aus dem virtualenv, 14,27 s
statt 1,32 s); test_datenquellen_mapping_sync.py hatte gar keinen venv-Filter. Beide meldeten
grün — ein Abwesenheitsbeweis über fremden site-packages-Code behauptet mehr, als er weiß.
Der Cache ist der Nebeneffekt, nicht der Zweck.test_wurzelmuster_konformitaet.py ruft seine
Dateiquelle sechzehnmal auf: 27,36 s → 8,93 s. Über alle Prüfer zusammen 51,96 s → 18,01 s,
bei unveränderter Fallzahl.
⚠ Kein Export dieser Datei heißt test… — unter dem Namen testbaum hat pytest die Funktion
beim Importeur als Testfunktion eingesammelt (python_functions = test* greift auf jeden Namen
im Modul-Namensraum) und als „grün” gezählt, ohne dass sie etwas prüfte. Sie heißt deshalb
probenbaum; test_quellbaum.py::test_kein_export_heisst_wie_eine_probe hält es fest.
Wer einen neuen Wächter baut, prüft auch seine Prüfmenge.quellbaum liefert
nicht_parsebar() — Dateien, die ast.parse nicht annimmt. Ein Prüfer, der sie still überspringt,
verliert Deckung, ohne es zu melden (die N-318-Klasse). Heute ist die Liste leer, und ein
Selbsttest hält sie leer.
Einen Backend-Test schreiben: die Factories benutzen
backend/tests/factories.py baut die Modelle, conftest.py liefert die db-Fixture. Neue Tests
nutzen die Factories, alte werden bei Berührung umgehängt — nicht in einem Zug.
from backend.tests import factories
asyncdeftest_etwas(db):
a =await factories.anlage(db,standort_land="DE") # + flush, `a.id` steht bereit
Zwei Formen:mach_* konstruiert nur (ohne Session), die kurzen Namen legen an und flushen.
commit ruft der Test selbst, damit er sichtbar bleibt. Wiederkehrende Aufbauten aus mehr als
einem Modell stehen als Szenarien daneben (anlage_mit_pv · anlage_mit_tarif ·
anlage_mit_modul · zwei_wechselrichter · mach_anlage_mit_mapping).
⚠ Defaults nur für das technisch Nötige, nie für fachliche Werte.anlage() setzt Namen und
kWp (98 % bzw. 96 % aller Konstruktionen tun das, kein Test behauptet sie) — aber keinstandort_land und keine Tarifpreise: eine Factory, die einen Tarif erfindet, hält genau den
Test still grün, der den Tarif behaupten wollte. test_factories.py hält das fest.
Werte-Fakten (§3, seit E6):mach_monats_fakt() und mach_kennzahlen() bauen einen
MonatsFakt (ADR-002/P10) bzw. VerbrauchsKennzahlen. Beide verlangen alle Felder als
Pflichtargumente — acht Teil-Fakten bzw. sieben Kennzahlen. Wer nur eine Größe behaupten will,
baut sonst den Rest von Hand und muss ihn anfassen, sobald ein Teil-Fakt dazukommt:
from backend.tests.factories import mach_kennzahlen, mach_monats_fakt
Der Backend-Testbaum ist flach — 409 Dateien in backend/tests/, ohne Themen-Ordner.
Das ist eine Entscheidung, keine offene Baustelle (Gernot, 2026-08-24): am Baum gemessen
tragen 135 der 409 Dateien (33 %) mehr als ein Thema im Namen —
test_ha_export_wp_spezialtarif.py gehörte gleichzeitig nach ha/, waerme/, finanzen/ und
import_export/. Ein Themen-Ordner wäre eine dritte Konvention über den Feature-Namen und
träfe für jede dieser Dateien eine Wahl, die der nächste Sucher nicht nachvollziehen kann.
Stattdessen gilt die Regel, die ohnehin schon galt — jetzt gewächtert:
Neue Datei in den bestehenden Präfix-Cluster einordnen, keine dritte Namensvariante für
dasselbe Modul schaffen (HAStatisticsService → einheitlich test_ha_lts_*).
Im Modul-Docstring mindestens eine Schwesterdatei nennen.
Warum das mehr ist als Kosmetik. Namens-Drift erzeugt Lücken aus beiden Suchrichtungen:
ha_statistics_service.py galt einmal als „0 Tests”, weil die Suche test_ha_statistics*
lautete und die Familie test_ha_lts_* heißt. Der Fehlbefund floss in einen Refactoring-Plan
ein. Ein Docstring, der eine Schwester nennt, macht das Set von jedem Einstiegspunkt aus
begehbar. Deshalb misst man Testabdeckung auch per Symbol, nie per geratenem Dateinamen.
Der Wächter ist test_konformitaet_schwesterdateien.py. Er verlangt den Verweis nur von Dateien,
deren Präfix-Cluster mindestens zwei Dateien umfasst — ein Einzelgänger hat keine Schwester. Der
genannte Name muss existieren und darf nicht die Datei selbst sein (16 Bestandsdateien
nannten im Docstring ausschließlich sich; das ist eine Überschrift, kein Querverweis). Die
Schwester darf über den Cluster hinausgehen — ein Symmetriepartner ist oft der nützlichere
Hinweis. Die Baseline (272 Dateien) ist abschmelzend: sie heilt nichts, sie verhindert die
273., und ein erledigter Eintrag ist selbst ein Fehler.
Das Recorder-Schema von Home Assistant (statistics_meta · statistics ·
statistics_short_term) steht einmal — in backend/tests/ha_lts_helfer.py. Es ist ein
fremdes Schema; vier handgeschriebene Kopien driften, sobald HA eine Spalte anfasst.
has_mean folgt standardmäßig aus has_sum — ein Zähler trägt keinen Mittelwert, ein Messwert
umgekehrt. zeile() schreibt wahlweise nach statistics oder statistics_short_term
(tabelle=).
⚠ Kein Export eines Helfer-Moduls darf mit test beginnen — python_functions = test*
greift auf jeden Namen im Modul-Namensraum des Importeurs; pytest sammelte so schon einmal
eine Quell-Funktion als Testfunktion ein und zählte sie grün.
Einen Frontend-Test schreiben: die Factories und renderMitProvidern
src/test/factories.ts baut die drei großen API-Antworten, src/test/render.tsx den
Provider-Turm. Neue Tests nutzen sie, alte werden bei Berührung umgehängt — nicht in
einem Zug, wie im Backend.
⭐ Warum typgebunden statt as unknown as.AktuellerMonatResponse hat 88 Pflichtfelder,
AggregierteMonatsdaten und TagWerte je 42 — von Hand schreibt das niemand aus, also stand in
38 Testdateien ein as unknown as X. Ein solcher Cast entkoppelt die Fixture vom Typsystem:
ein umbenanntes Feld im API-Client bricht dort keinen Test. Am realen Sprengsatz gemessen
(2026-08-24, je ein umbenanntes Pflichtfeld): AggregierteMonatsdaten.netzbezug_kwh meldeten
vorher 2 Testdateien, nachher 9; AktuellerMonatResponse.netzbezug_kwhvorher 3,
nachher 11. ⚠ Wer diese Messung wiederholt, ersetzt innerhalb des Interface-Blocks —
netzbezug_kwh: number steht in monatsdaten.ts dreimal, und ein globales Replace traf beim
ersten Versuch MonatsdatenCreate und meldete folgerichtig 0 betroffene Dateien. Das Basisobjekt der Factory erfüllt den Typ per satisfies, der
Aufrufer übergibt Partial<T> — ein Tippfehler darin ist ein Compile-Fehler statt eines stillen
undefined.
⚠ Die Defaults behaupten nichts — dieselbe Regel wie im Backend. Jede Menge steht auf ihrer
Nullstellung (null wo der Typ es zulässt, sonst 0/false/{}/[]), Identität ist
Parameter, nicht Default. factories.test.ts hält das mit einem Wächter fest, der jedes
Feld auf erfundene Zahlen und Texte absucht.
⚠ Nullstellung ist nicht undefined. Der handgebaute Cast ließ ungenannte Felder
undefined; die Factory setzt sie auf null/0. Für ?? ist das gleich, für !== undefined
und Object.keys nicht — wer eine Bestandsdatei umhängt, misst ihre Vitest-Fallzahl vor und
nach dem Eingriff. ⛔ tsc ist dabei der einzige Prüfer, der zählt: Vitest strippt die
Typen, ESLint kennt den fehlenden Typ-Import nicht — ein bei der Umstellung mitentfernter
import type fiel am 24.08. nur tsc --noEmit auf, nachdem lint und Vitest grün gemeldet
hatten.
Backend-Regeln haben pytest als Wächter, keine check:*-Skripte (die sind alle Frontend-Node):
test_berechnungs_layer_konformitaet.py (ADR-001) ·
test_wurzelmuster_*.py (ADR-002). Zwei Ausnahmen bewachen die
Client-Hälfte einer Backend-Regel: check:kennwert-roh (P3-a) und check:co2-roh (DI-2).
Neue Proben rot verifizieren. Ein Test, der gegen den alten und den neuen Stand grün ist,
beweist nichts. Der Sprengsatz muss außerhalb eines except Exception zünden — sonst
verschluckt der Catch ihn und die Probe bleibt stumm.
Bleibt eine Probe grün, wird sie geschärft, nicht weggelassen. Häufigste Ursache: die Fixture
bildet den Fehlerfall nicht ab (etwa „doppelte Daten” geprüft, während der Fehler verschiedene
Daten mit gleichen Werten erzeugt).
Fixtures fremder APIs brauchen eine benannte Quelle im Docstring. Eine selbstgebaute
Antwortform ist eine Behauptung über einen fremden Server; Rot-Verifikation prüft nur, ob der
Test den eigenen Code greift.
Hermetisch heißt auch: ohne echte Uhr und ohne Systemzeitzone.vitest.config.ts pinnt
Europe/Berlin; im Backend gehört die Zeit in die Fixture. Eine Probe, die in vier von
24 Stunden fällt, macht jeden grünen Lauf zum Zufallsbefund.
Vor dem Ändern einer Meldung im tests/-Baum nach dem alten Wortlaut greppen — ein
Wortlaut-Filter macht Negativ-Tests stumm.
Mount-Proben der V4-Sichten leeren den SWR-Cache (_clearSwrCacheForTests); er ist ein
Modul-Singleton und trägt sonst Werte in die nächste Probe.
Gates ≠ CI. Die Gates laufen auf einer Maschine mit ihren Paketversionen und ihrer
Uptime; der GitHub-Runner ist frisch. Ein grüner Gate-Lauf ist die Voraussetzung für ein
Release, nicht die Bestätigung, dass CI grün wird — und ein rotes CI ist nicht automatisch ein
Testproblem, sondern erst nach Ursachen-Beleg.
Erhoben aus backend/main.py (die include_router-Aufrufe), Stand 2026-08-07. Die Prefixe
sind dort der SoT — ein Modulname sagt nichts über seinen Prefix, und mehrere Module teilen sich
einen. Im Zweifel: grep include_router backend/main.py.
Portal-CSV-Import — ⚠ Modul heißt data_import, Prefix portal-import
/api/custom-import
custom_import/
CSV/JSON mit Feld-Mapping
/api/import
import_export/
CSV, JSON, Demo-Daten, PDF
/api
monatsabschluss/, community, ha_export
drei Module ohne eigenes Prefix — ihre Pfade stehen im Modul selbst
/api/energie-profil
energie_profil/
Stunden-/Tagesprofile, Reaggregation, Tag-Status
/api/repair
repair
Reparatur-Werkbank (Tag/Zeitraum neu rechnen)
/api/system
system_logs, daten_checker
Logs und Daten-Checker — zwei Module, ein Prefix
/api/diagnostics
diagnostics
Diagnose-Endpunkte
/api/dokumentation
dokumentation
PDF-Dokumente (Anlagendoku, Finanzbericht)
/api/infothek
infothek
Komponenten-Akten, Verträge, Datei-Upload (N:M)
/api/ha
ha_remote, (HA_MODE)ha_integration
HA-Verbindung/Token · HA-Status
/api/sensor-mapping
(HA_MODE)sensor_mapping
nur noch /{id}/suggest (HA-Energy-Vorschläge, #197). Die fünf Zuordnungs-Endpunkte sind seit 2026-08-13 stillgelegt (N-241) — Zuordnung läuft über /api/datenquellen
Hinweis: die mit (HA_MODE) markierten Router werden nur eingehängt, wenn HA_MODE=true.
/api/ha gibt es in beiden Betriebsarten — ha_remote (Verbindung + Token für den
Standalone-Container) hängt nicht am Add-on-Modus.
docs/ ist der SoT. Zwei generierte Kopien hängen daran, und beide werden nicht von Hand
editiert:
Ziel
Erzeugt von
Zustand im Repo
Website (Astro Starlight, GitHub Pages)
website/scripts/sync-docs.sh (läuft als prebuildim website/-Verzeichnis)
website/src/content/docs/ ist gitignored
In-App-Hilfe
scripts/sync-help.sh
die Kopien sind versioniert und gehören in denselben Commit
Nach jeder Doku-Arbeit scripts/sync-help.sh laufen lassen und die Kopien mitcommitten —
sonst zeigt die Hilfe im Add-on einen anderen Text als das Repo. Die In-App-Hilfe ist kein Archiv:
WAS-IST-NEU.md wird darin zurückgeblättert, ein überholter Satz in einem alten
Abschnitt bleibt also sichtbar. Wird eine Aussage später widerlegt, kommt ein Korrektur-Vermerk
an den alten Eintrag; der historische Wortlaut bleibt stehen.
Eine Versionsnummer schreibt nur, wer sie kennt. Solange der Release-Entscheid nicht gefallen
ist, gehört in einen Doku-Absatz kein „ab v4.x” — die Zahl steht dann im CHANGELOG-Abschnitt,
nicht im Fließtext. Am 2026-08-06 mussten so 19 Behauptungen vor einem Release korrigiert werden,
14 davon „v4.1”, vier in ausgelieferter Anwender-Doku.
Konzept-Dokumente werden im Rumpf fortgeschrieben — der Kopf bleibt stehen. Genau dort
liest aber jeder zuerst. Am 2026-08-08 gemessen, drei Fälle derselben Klasse:
KONZEPT-SPEICHER-AUSWERTUNG.md nannte „Kern weiterhin offen” und verwies auf ein Issue, das
mit v4.0.0 geschlossen wurde — drei Absätze tiefer im selben Kasten stand „Phase 1
ausgeliefert”.
KONZEPT-UNVOLLSTAENDIGE-WERTE.md trug „VORSCHLAG — kein Code”, während die ersten beiden
Bausteine am selben Tag gebaut worden waren.
KONZEPT-HA-EXPORT-ARCHITEKTUR.md führte eine Restarbeit als offen, die längst erledigt war
(sw_version liest APP_VERSION, der tote Helper ist entfernt).
Die Regel: Wer ein Konzept anfasst, fasst seinen Status mit an und hält die offenen
Zeilen gegen den Code. Ein Statuskopf trägt ein Mess-Datum, keine Versionsnummer — eine
Zahl an dieser Stelle veraltet garantiert. Und: die offenen Zeilen eines publizierten Konzepts
sind selbst eine Arbeitsquelle — sie werden bei der Paket-Wahl mitgelesen, auch wenn kein Issue
dafür existiert.
docs/drafts/ ist gitignored (.gitignore:82) — die Dateien dort liegen nur auf der Maschine,
auf der sie entstanden sind. Ein Verweis dorthin ist für jeden Mitleser ein 404. Am 2026-08-08
baumweit gemessen: 16 Stellen im Produktionscode zeigten dorthin, zwei davon
(api/routes/datenquellen.py, hooks/useApiData.ts) mit dem Wort „SoT”.
Die Regel, die daraus folgt:
Ein Dokument, das im Code als SoT zitiert wird, gehört nach docs/ — nicht in drafts/.
Deshalb ist KONZEPT-DATENQUELLEN-V4.md dorthin gewandert, so wie vorher schon
KONZEPT-MONATS-FAKTEN.md.
⚑ Die Umkehrung gilt seit 2026-08-28 genauso (Entscheid Gernot): Ein Dokument, das nur
noch einen Bauplan enthält — Maßnahmen-Register, Pakete, offene Punkte —, gehört nicht
ins öffentliche Repository. docs/ ist die Beschreibung des gebauten Systems; Baupläne
und Vorhaben leben in docs/drafts/ (lokal). Wer ein solches Dokument hinausschiebt, löst
vorher jeden Verweis darauf auf: Die Regel wandert in den Docstring, das Dokument bleibt
höchstens Herkunftsangabe. So sind am 28.08. KONZEPT-FOKUS-DEEPLINK, KONZEPT-CHART-TABELLEN,
KONZEPT-COMMUNITY und IA-V4-SOT-INVENTAR gegangen — mit null verbliebenen Zeigern.
Bestehende Verweise auf docs/drafts/archive/… sind Herkunftsbelege, keine Voraussetzung.
Sie sagen, aus welchem Bau-Vertrag eine Regel stammt; die Regel selbst steht immer im
versionierten Baum (ADR, Konzept, Docstring). ADR-002 formuliert das für sich selbst
ausdrücklich so: „Diese ADR ist so geschrieben, dass sie ohne den Bericht trägt.” Wer eine
solche Stelle anfasst, prüft, ob die tragende Passage schon eine versionierte Heimat hat — und
hebt sie sonst dorthin.
Ein Verweis darf nicht ins Leere zeigen, auch nicht innerhalb von drafts/. Wandert ein
Dokument nach drafts/archive/, wandern seine Verweise mit (am 2026-08-08 waren acht Pfade
falsch, weil das Ziel längst archiviert war).
abgeschlossen und gebaut — zwei Bauform-Kapitel (Split-Klimaanlage · Multisplit-Innengeräte). ⚠ Nicht das Konzept der Fläche „Heizen · Warmwasser · Kühlen“ — das ist das Flächen-Konzept SOLL Wärme/Klima (Maintainer-intern, noch nicht im Repo). Beide Kapitel tragen im Kopf eine Korrekturliste. Bis 27.08. standen sie hier unter „geplante Pakete“
Wärme/Klima aus Anwendersicht — Erfassungswege, Werte des Modus-Sensors, Kennzahlen, FAQ. Auch für Entwickler die schnellste Antwort auf „was sieht der Melder?“