## Ergebnis der Read-only-Analyse ### Fehlerzuordnung der ursprünglichen 32 Backendfehler | Kategorie | Anzahl | Zuordnung | |---|---:|---| | 1. Schemaerwartung 45/46 | **18** | Acht einzelne Schemaassertions plus zehn Tests, die am gemeinsamen `db()`-Assert in `test_grocery_matching_learning_v2.py` scheiterten | | 2. Implizites `performance_included=1` | **12** | Zwölf Ingestion-/Reconciliation-/Performance-Tests verwendeten synthetische Konten ohne explizite Scopeklassifikation | | 3. Alte Analytics-Proxyperformance | **1** | Der große Migrations-/Analytics-Szenariotest; zunächst durch die alte Schemaassertion maskiert | | 4. Echte Regression | **1** | PostFinance-Sprint-13-Regression durch fehlende Klassifikation neu bestätigter Rollen | | **Gesamt** | **32** | | ### 1. Schemaerwartung 45 statt 46 — 18 Fehler Betroffene Dateien/Tests: - `tests/unit/test_budget_monthly_import_rule_learning_v1.py` - `tests/unit/test_budget_phase1.py` - `tests/unit/test_budget_phase11.py` - `tests/unit/test_budget_phase12_seed_review.py` - `tests/unit/test_budget_phase16_user_rules.py` - `tests/unit/test_fixed_costs_subscriptions_v1.py` - `tests/unit/test_grocery_price_providers_v1.py` - `tests/unit/test_schema.py` - `tests/unit/test_grocery_matching_learning_v2.py` - zehn Tests scheiterten bereits im gemeinsamen `db()`-Setup an `45 != 46`. Die Änderungen `45 → 46` sind hier sachgerecht und keine Abschwächung der Tests. ### 2. Implizites `performance_included=1` — 12 Fehler Alle lagen in: `tests/unit/test_portfolio_data_ingestion_reconciliation.py` Betroffene Tests: - `test_preview_is_storage_free_and_classifies_new_duplicate_blocked_and_unchanged` - `test_confirm_is_atomic_audited_idempotent_and_second_fresh_preview_is_unchanged` - `test_confirmation_identity_rejects_changed_payload_and_stale_source_revision` - `test_snapshot_correction_creates_new_immutable_version` - `test_canonical_activity_preview_handles_duplicates_ambiguity_and_unsupported_records` - `test_parallel_confirmation_produces_one_batch_and_one_snapshot` - `test_reconciliation_matches_quantity_value_cash_and_total_value_account_without_holdings` - `test_reconciliation_distinguishes_tolerance_mismatch_missing_fx_and_cutoff` - `test_scope_aware_raiffeisen_akb_transfer_is_internal_for_portfolio_and_external_for_account` - `test_multiple_same_day_values_receive_sequential_versions_in_one_batch` - `test_unconfirmed_transactions_are_excluded_from_ingestion_and_reconciliation` - `test_stale_position_price_and_fx_inputs_are_not_comparable_and_reduce_coverage` Ursache: Die Fixtures erwarteten weiterhin, dass ein Konto allein durch Anlage oder `performance_included=1` performancefähig sei. Die ergänzten synthetischen Audit- und Klassifikationszeilen sind fachlich richtig. ### 3. Alte Portfolio-Analytics-Proxyperformance — 1 Fehler Test: `tests/unit/test_portfolio_data_ingestion_reconciliation.py::test_migrations_41_to_43_are_additive_and_ingestion_history_is_immutable` Der Fehler war zunächst durch dessen alte Schema-45-Assertion maskiert. Danach zeigten sich zwei gekoppelte Ursachen: 1. Der Kurscache verwendete einen Kurs des vorherigen Geschäftstags auch für einen neuen normalen Handelstag und unterdrückte damit den Providerabruf. 2. Die alte Analytics-Projektion versuchte aus dem neuen kanonischen, wegen fehlender globaler Coverage nicht verfügbaren Performancevertrag weiterhin eine normalisierte Proxyserie abzuleiten. Während meiner Analyse wurde dieser Bereich parallel weiterbearbeitet: - `portfolio_analytics.py` kennzeichnet die Projektion nun als deprecated und bezieht Renditen aus `portfolio_performance_v2`. - `test_portfolio_market_analytics_v1.py` prüft kanonische TTWROR-Punkte sowie getrennte rohe Bewertungs-/Benchmarkpunkte. - Der gezielte Szenariotest ist auf dem aktuellen Stand **grün: 1 passed**. ### 4. Echte Regression — 1 Fehler Test: `tests/test_postfinance_sprint13.py::test_global_read_model_counts_official_postfinance_components_once` Symptom: Der erwartete PostFinance-Reconciliation-Eintrag fehlte. Ursache: Migration 46 klassifiziert nur bereits vorhandene Konten. PostFinance kann das E-Finance-Konto und Rollenbindungen aber erst beim späteren Confirm erzeugen. Ohne nachgelagerte Rollenklassifikation waren die neuen Konten für die performancegefilterte Reconciliation unsichtbar. Die aktuelle Richtung ist korrekt: - E-Finance wird mit `performance_included=0` angelegt. - E-Trading-Depot und Settlement-Cash werden beim bestätigten Rollenmapping explizit eingeschlossen. - E-Finance wird explizit als Kontrollkonto ausgeschlossen. - TrueWealth klassifiziert das kanonische Konto beim bestätigten offiziellen Import. --- ## Kritische aktuelle Migrations-/Scope-Risiken ### P0: Schema-Default wird bei realer Migration nicht geändert Die Änderung in `storage/schema.py` wirkt nur bei neu angelegten Datenbanken. Auf einer bestehenden Schema-45-Datenbank bleibt der SQLite-Spaltendefault unverändert. Read-only In-Memory-Nachweis: ```text migrated_version: 46 performance_default_after_45_to_46: 1 ``` Damit würde die produktive Datenbank trotz Migration 46 weiterhin neue Konten standardmäßig mit `performance_included=1` anlegen. **Empfehlung:** In Migration 46 die `accounts`-Tabelle sicher und additiv auf `DEFAULT 0` migrieren beziehungsweise den realen `PRAGMA table_info(accounts)`-Default als Postcondition prüfen. Ein Test muss explizit eine simulierte Schema-45-Datenbank migrieren, nicht nur eine neue In-Memory-Datenbank aus aktuellem `INITIAL_SCHEMA_SQL`. ### P0: INSERT umgeht das Klassifikations-Gate Der Trigger `accounts_performance_include_requires_classification` schützt nur: ```sql BEFORE UPDATE OF performance_included ``` Ein direktes: ```sql INSERT INTO accounts(..., performance_included) VALUES(..., 1) ``` wird nicht blockiert. Genau dieses Muster existiert weiterhin in Import-/Testpfaden. Besonders kritisch: `src/jarvis_finance/imports/accounts_importer.py:52` ```python parse_bool(row.get("performance_included"), True) ``` Der generische Kontoimport nimmt also weiterhin `True` als Fallback und schreibt das Flag direkt beim INSERT. **Empfehlung:** - Importer-Default auf `False` ändern. - Generische Kontoimporte dürfen `performance_included=1` nicht direkt setzen. - Neue Konten zuerst mit 0 anlegen; Einschluss ausschließlich über den Klassifikationsservice. - Zusätzlich INSERT-seitige Datenbank-Guardrail ergänzen. ### P0: Performanceengine verwendet weiterhin primär das Flag `portfolio_performance._account_ids()` filtert nur nach: ```sql accounts.performance_included=1 ``` Es erfolgt kein Join auf: - `performance_scope_classifications` - `decision_version='investment_performance_scope_v1'` - freigegebene `classification_role` Damit kann ein per INSERT gesetztes oder anderweitig gedriftetes Flag weiterhin die Engine erreichen. **Empfehlung:** Kontoauswahl aus Flag **und** passender aktueller Klassifikation ableiten; bei Widerspruch fail-closed mit Reason Code. Das Flag sollte Cache/Projektion sein, nicht alleinige Autorität. ### P0/P1: Crypto-Sperre ist global und dauerhaft hartcodiert Sobald irgendeine Scopeklassifikation v1 existiert, leert `build_portfolio_performance()` bei globaler Abfrage pauschal alle Punkte: ```python points = [] missing_crypto_valuation_history ``` Die Abfrage prüft weder tatsächliche Crypto-Historie noch vollständige Domain-Coverage. Auch künftig vorhandene Crypto-Historie würde diese Sperre nicht automatisch aufheben. **Empfehlung:** Crypto-Coverage anhand tatsächlicher historischer Crypto-Bewertungen prüfen. Nur fehlende Coverage darf sperren, nicht die bloße Existenz einer Klassifikationstabelle. ### P1: Klassifikation selbst ist veränderbar Unveränderlich geschützt wird nur der zugehörige `audit_log`. Auf `performance_scope_classifications` fehlen UPDATE-/DELETE-Trigger. Der Service verwendet zudem `ON CONFLICT ... DO UPDATE`, überschreibt also den aktuellen Klassifikationsdatensatz. Risiken: - Direkte SQL-Änderungen ohne Audit. - Verlust der Klassifikationshistorie in der Klassifikationstabelle. - Wiederholte A→B→A-Entscheidungen können auf einen alten deterministischen Auditdatensatz zurückverweisen. - Klassifikationszeile und Accountflag können auseinanderlaufen; eine idempotente Servicewiederholung repariert diesen Drift derzeit nicht zwingend. **Empfehlung:** Append-only Entscheidungshistorie plus separate aktuelle Projektion oder zumindest Mutationsschutz und explizite Driftprüfung einführen. ### P1: TrueWealth-Kanonizität ist zu breit Migration 46 schließt jedes Konto ein, das einen aktiven, nicht aktualisierten Snapshot mit: ```sql source_type='truewealth_official_import' ``` besitzt. Bei mehreren solchen Konten würden mehrere TrueWealth-Konten eingeschlossen, obwohl der Vertrag genau ein kanonisches Totalwertkonto verlangt. **Empfehlung:** Gegen die bestätigte kanonische Portfolio-/Account-Zuordnung prüfen und eine Eindeutigkeits-Postcondition verlangen; bei 0 oder mehr als 1 Kandidat fail-closed migrieren. ### P1: Migrationsatomizität und Marker-Reihenfolge Die Scopefunktion wird innerhalb der Compat-Migration ausgeführt; der Schema-46-Marker wird erst danach geschrieben. Mehrere `executescript`-Aufrufe erschweren eine nachweislich atomare Transaktion. Existiert der Marker nach einem inkonsistenten Teilzustand, führt die Funktion keinen Backfill mehr aus. **Empfehlung:** Scope-Daten, Accountflags und Schema-Marker in einer klaren atomaren Migration schreiben und folgende Postconditions vor Commit prüfen: - Default tatsächlich 0; - jedes aktive Flag 1 besitzt genau eine freigegebene v1-Klassifikation; - keine ausgeschlossene Rolle besitzt Flag 1; - genau ein kanonisches TrueWealth-Konto; - PostFinance Depot und Settlement-Cash enthalten; - E-Finance ausgeschlossen; - Wiederholung ist ein echter No-op. ## Verifikationsstand - Historischer Ausgangslauf: **680 bestanden, 32 fehlgeschlagen**. - Mein erster Lauf auf dem vorgefundenen Zwischenstand: **711 bestanden, 1 fehlgeschlagen**. - Nach der parallel erfolgten Analytics-Anpassung: gezielter verbleibender Szenariotest **1 bestanden**. - Eine vollständige Vollsuite des allerneuesten, parallel veränderten Stands habe ich nicht erneut ausgeführt. ## Side Effects - **Keine Dateien erstellt oder geändert.** - Keine produktive Datenbank geöffnet oder migriert. - Nur Git-Diff-/Quelltextanalyse, In-Memory-Migrationsprobe und read-only Testläufe. - `git diff --check` ist grün.