## Ergebnis **P0/P1/P2 = 0/4/4** ### P1 – Hohe Priorität 1. **Falsche CHF-/%-Totals bei unvollständig erfasstem Vergleichsjahr** **Dateien:** - `src/jarvis_finance/services/budget_planning.py:808-829` - `frontend/src/pages/BudgetPlanningPage.vue:193-203` `comparison_total` summiert nur Kategorien mit erfasstem historischem Wert. `forecast_total` enthält dagegen alle aktiven Ausgabenkategorien. `totals_comparable` prüft nicht, ob `missing_comparison_category_count == 0`. Dadurch werden eine Abweichung und Prozentzahl zwischen vollständiger Prognose und unvollständigem Vergleichstotal berechnet und als „Total“ angezeigt. Reproduziertes Gegenbeispiel: Vergleichswert nur für eine Kategorie `CHF 1’200`, zweite Kategorie fehlt; ausgegeben wurden dennoch `deviation_chf=2556.00` und `deviation_percent=213.00` bei zehn fehlenden Vergleichskategorien. 2. **Unkategorisierte bestätigte Ausgaben fehlen in Matrix-Totals und werden im Jahresassistenten nicht prognostiziert** **Datei:** `src/jarvis_finance/services/budget_planning.py:600-607, 643-650, 808-815, 1193-1215` Die Matrix bildet ausschließlich aktive Expense-Kategorien ab. Bestätigte kanonische Ausgaben ohne `category_id` – ebenso Ausgaben einer inzwischen inaktiven Kategorie – fehlen deshalb vollständig in `actual_year_to_date_chf` und `forecast_year_chf`. Der Jahresassistent addiert unkategorisierte Ausgaben lediglich als bisherigen YTD-Betrag, prognostiziert aber keinen Rest und kann das Ergebnis trotzdem als `reliable=true` kennzeichnen. Reproduziert mit vier bestätigten unkategorisierten Buchungen à CHF 100: - kanonisches Ist: `CHF 400.00` - Matrix-Ist: `CHF 0.00` - Jahresprognose: `CHF 400.00` - dennoch `reliable=true` 3. **Preview-Concurrency-Guard schützt nicht atomar vor parallelen Überschreibungen** **Datei:** `src/jarvis_finance/services/prior_year_actuals.py:172-216` `source_data_version` und Fingerprint werden vor dem ersten DML geprüft. Zwischen dieser Prüfung und den `DELETE`/`INSERT`-Operationen gibt es weder `BEGIN IMMEDIATE` noch eine SQL-seitige Compare-and-swap-Bedingung. Zwei Requests können denselben Stand prüfen und anschließend sequenziell schreiben; der spätere Request überschreibt den früheren, ohne `409`. Damit kann ein bestätigtes, auditiertes Sammelspeichern durch einen parallelen veralteten Confirm verloren gehen. 4. **Out-of-order Loads können Vorjahreswerte unter dem falschen Jahr speichern** **Datei:** `frontend/src/pages/BudgetPlanningPage.vue:51-62, 70-78, 91-104` Mehrere `load()`-Aufrufe werden weder abgebrochen noch über Request-ID/Jahr gebunden. Wenn beispielsweise 2024 und kurz danach 2023 gewählt werden, kann die langsamere 2024-Antwort zuletzt `prior`, `matrix` und `draft` überschreiben, während `comparisonYear` bereits `2023` ist. Der nächste Save sendet dann die angezeigten 2024-Werte mit `year: 2023`. Die Jahresauswahl bleibt während des Ladens aktiv. ### P2 – Mittlere Priorität 1. **Unbeschränkte numerische Eingaben führen zu HTTP 500 statt 422** **Datei:** `src/jarvis_finance/services/prior_year_actuals.py:22-42` Sehr große Dezimalwerte wie `1e999999` oder eine 10.000-stellige Zahl passieren teilweise die Decimal-Konvertierung, scheitern aber unkontrolliert beim `quantize()` mit `decimal.InvalidOperation`. Ein Vergleichsjahr mit mehr als rund 4.300 Ziffern scheitert unkontrolliert bei `int(text)`. Beide Fälle ergeben Serverfehler und bilden eine vermeidbare API-/DoS-Oberfläche. Länge, Exponent und maximaler CHF-Wert sollten vor der Konvertierung begrenzt werden. 2. **Frontend-Typen entsprechen den tatsächlichen API-Antworten nicht** **Dateien:** - `frontend/src/api/budget.ts:14-16` - `src/jarvis_finance/services/prior_year_actuals.py:101-113, 217-223` `PriorYearActuals` verlangt `source_data_version`, das GET liefert dieses Feld nicht. `PriorYearActualsConfirm` verlangt `year`, der Confirm liefert nur `entity_id`. Die Tests maskieren den Drift durch entsprechend erfundene Mock-Felder. Aktuell greift die Seite nicht darauf zu, aber andere typisierte Consumer erhalten einen falschen Vertrag. 3. **Explorer-Formel ist mit den dargestellten Operanden nicht exakt reproduzierbar** **Datei:** `src/jarvis_finance/services/budget_planning.py:647-650, 737-739` Der Forecast verwendet den ungerundeten Monatsdurchschnitt, zeigt aber nur den auf Rappen gerundeten Durchschnitt an. Beispiel: - angezeigt: `33.33 × 8.50` - daraus nachvollziehbar: `283.30` - API zeigt Forecast-Rest: `283.33` Die Endsumme reconciliert zwar mit dem separat ausgegebenen Rest, die erklärte Multiplikation jedoch nicht. Für eine auditierbare Erklärung sollte entweder mit dem angezeigten Wert gerechnet oder mehr Präzision/ungekürzte Berechnungsbasis geliefert werden. 4. **Explorer meldet keine kategorienbezogenen, tatsächlichen Ausschlusszahlen** **Dateien:** - `src/jarvis_finance/services/budget_planning.py:894-910` - `frontend/src/pages/DataExplorerPage.vue:35` Der Transfer-Count zählt sämtliche bestätigten Transfers des Jahres, nicht nur solche der geöffneten Kategorie bzw. des dargestellten Scopes. Der Kreditkarten-Count zählt sämtliche Kandidaten mit entsprechender Klassifikation unabhängig von Status oder tatsächlicher Materialisierung. Die Anzeige „Ausgeschlossen: X Transfers und Y Kreditkarten-Ausgleiche“ suggeriert dagegen eine exakt zur Kategorieprognose gehörende Ausschlussmenge. ## Verifikation - Backend-Fokustests: **12 passed** - Frontend-Fokustests: **8 passed** - `vue-tsc`: **grün** - Ruff für geänderte Backend-Dateien: **grün** - `git diff --check`: **grün** - Zusätzliche read-only Gegenbeispiele für unvollständige Totals, unkategorisierte Ausgaben, Rundungsreconciliation und Extremwerte ausgeführt. **Dateien erstellt oder verändert:** keine. **Commits:** keine.