## Verdict: **BLOCK** ### P0 — 0 Keine P0-Findings. ### P1 — 4 1. **Immutable Dry-run kann aktuelle WAL-Änderungen ignorieren** - **Evidenz:** `src/jarvis_finance/storage/database.py:19-24`, verwendet durch `src/jarvis_finance/cli/main.py:183-190`. - `connect_read_only()` setzt standardmäßig `immutable=1`. Bei einer laufenden SQLite-WAL-Datenbank kann SQLite dadurch committed WAL-Inhalte ignorieren. Synthetischer Gegenprobe: normaler `mode=ro`-Reader sah 1 Zeile, `immutable=1` sah 0. - **Auswirkung:** Der Dry-run kann veraltete Holdings oder Provider-IDs als „aktuell“ bewerten. - **Fix:** Live-Dry-runs mit `mode=ro` plus `PRAGMA query_only=ON`, aber ohne `immutable=1`, öffnen. `immutable=1` ausschließlich für nachweislich eingefrorene Kopien verwenden; WAL-Gegenprobe ergänzen. 2. **Quote-Identität und Währung werden nicht fail-closed validiert** - **Evidenz:** `src/jarvis_finance/services/crypto_market_recovery.py:152-169`, anschließend persistiert als CHF in `:443-467`. - Es werden nur Dictionary-Key, Preis, Qualitätsstatus und Timestamp geprüft. `quote.coingecko_id`, `quote.currency` und `quote.provider` werden nicht gegen die kanonische CoinGecko-Anfrage validiert. - Gegenprobe: Eine Quote mit internem ID `ethereum`, Währung `USD` und Provider `NotCoinGecko`, geliefert unter Key `bitcoin`, wurde als `complete` akzeptiert und als CHF/CoinGecko-ID `bitcoin` gespeichert. - **Auswirkung:** Fremdwährungs- oder falsch zugeordnete Preise können eine materiell falsche CHF-Bewertung erzeugen. - **Fix:** Pro Quote zwingend `quote.coingecko_id == requested_id`, `quote.currency.upper() == "CHF"` und kanonischen Provider `CoinGecko` prüfen; jede Abweichung als Blocker behandeln und null Preis-/Bewertungszeilen schreiben. 3. **Crypto-Aktivierung ist bei Parallelaufrufen nicht idempotent** - **Evidenz:** `src/jarvis_finance/services/crypto_market_recovery.py:249-268`. - Der Idempotenz-Check erfolgt vor `BEGIN IMMEDIATE` und wird innerhalb der Transaktion nicht wiederholt. In einer realen Zwei-Connection-Gegenprobe erzeugten zwei identische parallele Bestätigungen zwei Auditzeilen; beide Antworten meldeten `idempotent=False`. - **Auswirkung:** Der source-spezifische Aktivierungsgate besitzt keine eindeutige, verlässliche Auditidentität. - **Fix:** Erst `BEGIN IMMEDIATE`, dann innerhalb des Locks erneut nach `(source, action, confirmation_id)` suchen; bei Treffer idempotent zurückgeben. Paralleltest mit zwei DB-Verbindungen ergänzen. 4. **Der gemeinsame Tagesjob kann trotz Sprint-Grenze PostFinance und True Wealth schreiben** - **Evidenz:** `src/jarvis_finance/cli/main.py:283-338`. PostFinance und zwei True-Wealth-Writer bleiben im ausgeführten Worker-Set. Gleichzeitig deklariert die Statusprojektion PostFinance als inaktiv: `src/jarvis_finance/services/performance_activation.py:40-44`. - Bestehende Performance-Aktivierungen reichen über `run_activated_daily_source()` aus, um diese Writer auszuführen, sobald das globale Tagesjob-Gate für Crypto aktiviert wird. - **Auswirkung:** Aktivierung des Crypto-Pfads kann unbeabsichtigt PostFinance-/True-Wealth-Cashflow- oder Bewertungswrites auslösen; das widerspricht „postfinance/truewealth inactive“. - **Fix:** Sprint-20E-Tagesjob auf Crypto allein begrenzen oder separate, standardmäßig false source-spezifische Gates für PostFinance und True Wealth verlangen. Statusprojektion und tatsächlich ausführbare Worker müssen dieselbe Aktivierungsquelle verwenden. ### P2 — 3 1. **Status mischt Kennzahlen verschiedener Läufe** - **Evidenz:** `src/jarvis_finance/services/performance_activation.py:108-117`. - `valued_assets` stammt vom letzten erfolgreichen Lauf, `missing_assets` dagegen vom jüngsten Versuch. Nach einem erfolgreichen Lauf mit 2 Assets und einem fehlgeschlagenen Lauf mit 3 Assets kann die UI „2 bewertet · 3 fehlend“ anzeigen. - **Fix:** Beide Zähler aus demselben Lauf ableiten oder explizit getrennte Felder für „letzter Erfolg“ und „letzter Versuch“ modellieren. 2. **Preis-Provenienz des Status ist nicht laufgebunden** - **Evidenz:** `src/jarvis_finance/services/performance_activation.py:79-85`. - Die Abfrage nimmt sämtliche frischen `crypto_prices` zwischen Start und Ende des Runs auf. Ein paralleler Legacy-Refresh mit separatem Lock kann `price_as_of` verfälschen. - **Fix:** Preise eindeutig an den Run binden; ohne Schemaänderung mindestens den exakten gemeinsamen `fetched_at == started_at`-Wert und kanonischen Provider verwenden. 3. **Unerwartetes untracked Release-Artefakt** - **Evidenz:** `uv.lock` ist im final beobachteten Tree untracked, 335 559 Bytes/1 717 Zeilen, aber nicht Teil des initialen Sprint-Diffs. - **Fix:** Vor Freeze entfernen oder ausdrücklich in den genehmigten Scope aufnehmen und separat reviewen. **Gesamt:** P0 **0**, P1 **4**, P2 **3** — **BLOCK**. Keine Dateien durch den Review absichtlich geändert; Produktion wurde nicht geöffnet oder ausgeführt. Fokussierte bestehende Tests waren grün, decken die oben reproduzierten Gegenbeispiele jedoch nicht ab.