## Outcome - Inspected the read-only worktree at commit `c332f4c`. - No production data, runtime files, reports, or secrets were inspected. - No files were created or modified; `git status`, `git diff --stat`, and `git diff --check` were clean. ## Migration/schema inventory - Current migration head: - `src/jarvis_finance/storage/migrations.py` - `MIGRATION_VERSION = 43` - `MIGRATION_NAME = "043_postfinance_baseline_mapping_audit_v1"` - Next migration number is **44**. - Recent sequence: - 39: portfolio policy foundation - 40: portfolio performance foundation - 41: portfolio ingestion/reconciliation - 42: daily market/FX/benchmark/risk - 43: PostFinance baseline mapping audit - Migration mechanism: - `apply_migrations()` runs `INITIAL_SCHEMA_SQL`, then every compatibility creator in `_apply_compat_migrations()`, and finally records only the current migration version. - A v44 change must therefore be additive/idempotent and be added to `_apply_compat_migrations()`. - `tests/unit/test_schema.py::test_schema_version_recorded` and `tests/unit/test_portfolio_data_ingestion_reconciliation.py::test_migrations_41_to_43_are_additive_and_ingestion_history_is_immutable` currently assert version 43. ### Reusable tables - `account_value_snapshots` - Current manual TrueWealth read model. - Fields include `snapshot_id`, `account_id`, `valuation_date`, `total_value_chf`, `source_type`, `quality_status`, `notes`, timestamps. - **Not immutable** and has no source-file revision/idempotency constraint. - `portfolio_valuation_snapshots` - Canonical immutable/versioned account valuation. - Supports `snapshot_version`, `supersedes_snapshot_id`, `source_reference`, quality and reason codes. - Protected by update/delete triggers. - `portfolio_ingestion_batches` - Immutable confirmed-batch envelope. - Includes `source_revision`, `input_fingerprint`, `preview_id`, unique `confirmation_id`, `payload_hash`, audit ID, counts and confirmation metadata. - `portfolio_ingestion_items` - Immutable row-level lineage/disposition history. - Includes source fingerprint/ref, record kind, disposition, target, lineage hash and redacted summary. - Existing reported-holding/control tables: - `positions_snapshot` - `cash_account_snapshots` - `cash_balances` - Older broker-review workflow: - `broker_import_dry_runs` - `broker_import_review_items` - `broker_import_execution_plans` - `instrument_mappings` - `platform_account_mappings` ### Important schema caveat `portfolio_valuation_snapshots` and ingestion batch/items already provide the immutable/versioned/audited foundation. By contrast, `account_value_snapshots` is a mutable legacy/display table. If TrueWealth verified snapshots are copied there for compatibility, v44 should either: 1. add source provenance plus partial immutable triggers for `source_type='truewealth_verified_snapshot'`, or 2. preferably make `_latest_truewealth_value()` read the immutable canonical valuation first and retain `account_value_snapshots` only as a legacy/manual fallback. ## Best reusable import pattern: PostFinance ### Parser - `src/jarvis_finance/imports/postfinance_etrading.py` - Key types/functions: - `PostFinanceDocument` - `PostFinancePosition` - `PostFinanceCash` - `parse_postfinance_portfolio(raw: bytes)` - Useful safeguards: - parses in memory; - content SHA-256; - compressed/uncompressed size limits; - ZIP path/encryption checks; - XML entity/doctype rejection; - deterministic row hashes; - exact Decimal handling; - no raw file or filename persistence. ### Preview/confirm service - `src/jarvis_finance/services/portfolio_data.py` - Core reusable functions: - `preview_ingestion()` - `confirm_ingestion()` - `_preview_payload()` - `_valuation_disposition()` - `_existing_lineage()` - `_insert_valuation()` - `_postfinance_document()` - `_postfinance_records()` - `_insert_postfinance_record()` - `ingestion_history()` - `list_data_sources()` - Existing guarantees: - preview is storage-free; - 15-minute preview TTL; - preview/input/payload hashes; - `BEGIN IMMEDIATE`; - confirm recomputes the preview; - confirmation-ID replay validation; - source-file hash replay protection for PostFinance; - full rollback on exceptions; - batch and item audit lineage; - redacted refs and summaries. ### API contract - `src/jarvis_finance/api/routers/overview.py` - `POST /api/portfolio/ingestion/preview` - `POST /api/portfolio/ingestion/confirm` - `GET /api/portfolio/ingestion/history` - `GET /api/portfolio/data-sources` - `src/jarvis_finance/api/schemas/portfolio_data.py` - `IngestionPreviewRequest` - `IngestionConfirmRequest` - `IngestionPreviewResponse` - `IngestionConfirmResponse` - `src/jarvis_finance/api/main.py` - Preview is explicitly allowlisted in `READ_ONLY_POST_PATHS`. - Confirm remains behind fail-closed write-mode checks. - Generic router endpoints can be reused; no new router is required. ### Transaction/audit helpers - `src/jarvis_finance/audit/log.py::record_audit_event` - `src/jarvis_finance/storage/units_of_work.py::unit_of_work` - Strongest existing atomic pattern is directly in: - `portfolio_data.py::confirm_ingestion` - `portfolio_data.py::confirm_postfinance_instrument_mappings` - Correction helpers: - `src/jarvis_finance/ledger/corrections.py::void_transaction` - `record_correction_audit` - Post-confirm rollback is currently logical/append-only, not deletion. There is no ingestion rollback endpoint. ## Current TrueWealth logic ### Manual placeholder model - `src/jarvis_finance/services/manual_entry_service.py` - `_is_truewealth_account_row()` - `_account_portfolio_meta()` - returns `("manual_total_value", "truewealth", False)`. - `_is_manual_total_value_account()` - `preview_account_value()` - `confirm_account_value()` - writes `account_value_snapshots` with `source_type='manual_total_value'`; - writes an `account_value_confirm` audit. - `preview_position()` - explicitly rejects individual holdings for managed/TrueWealth accounts. - API: - `src/jarvis_finance/api/routers/positions.py` - `POST /api/accounts/{account_id}/value/preview` - `POST /api/accounts/{account_id}/value/confirm` - Read model: - `src/jarvis_finance/services/portfolio_service.py::_latest_truewealth_value` - selects the latest `account_value_snapshots` row by TrueWealth-like name/type. - Tests: - `tests/unit/test_cash_truewealth_management.py` - `tests/unit/test_vue_dashboard_v12_acceptance.py` ### Older structural TrueWealth importer - `src/jarvis_finance/imports/broker_parsers.py::parse_true_wealth_docx` - only detects row structure/flags; - does not extract verified values suitable for confirmed snapshot ingestion. - `src/jarvis_finance/imports/broker_mapping.py` - `run_broker_import_dry_run` - review-item mapping/readiness functions. - Important limitations: - `create_broker_import_dry_run()` hashes the **filename string**, not file bytes; - persisted summaries intentionally omit values; - `assert_productive_import_disabled()` documents that this workflow was historically non-productive; - therefore it should not be used as the authoritative file-idempotency layer. - `src/jarvis_finance/imports/execution.py::execute_initial_snapshot_plan` - can write reviewed initial-position transactions; - commits internally and is narrower/weaker than the newer portfolio ingestion transaction boundary. - Relevant tests: - `tests/unit/test_broker_parser_dry_run_pipeline.py` - `tests/unit/test_import_execution_and_corrections.py` - `tests/unit/test_broker_mapping_import_wizard.py` ## Critical double-counting risk The product baseline says TrueWealth remains a **managed aggregate position**: - `docs/finance-manager-2.0/sprint-0-baseline.md`, lines 23–25. Current overview computes: ```text cash + crypto + equity + latest TrueWealth account total ``` in `portfolio_service.py::get_overview()`. Therefore, importing TrueWealth holdings as active `transactions` while retaining its aggregate account total can double count: 1. `equity_service.list_equity_positions()` reads all stock/ETF ledger positions and does not exclude managed/TrueWealth accounts. 2. `get_equity_summary()` adds their market values to equity. 3. `_latest_truewealth_value()` independently adds the aggregate snapshot again. Only the old fallback `_postfinance_equity_value()` excludes TrueWealth-like accounts; the audited equity-summary path does not. **Minimal safe semantics:** import TrueWealth as immutable account-level valuations only. Do not write TrueWealth holdings into the active transaction ledger or equity total unless a larger design explicitly separates look-through/reporting holdings from owned canonical positions and changes all aggregation queries. Also preserve PostFinance behavior unchanged: add a separate source branch rather than weakening or generalizing `_postfinance_records()`. ## GET side-effect risks - Good existing pattern: - `portfolio_data.preview_ingestion()` is storage-free. - `cash_service.get_cash_summary()` explicitly does not call `ensure_canonical_cash_accounts()`. - `dashboard.data.get_positions()` calls `calculate_positions(..., persist_alerts=False)`. - `tests/unit/test_postfinance_portfolio_source.py::test_portfolio_read_does_not_refresh_accounts_updated_at` guards this. - New TrueWealth GET/read routes must not: - seed/update account metadata; - parse or import source files; - create alerts/audits; - call providers; - refresh timestamps; - commit. - Keep file parsing on the preview POST. GET history/status should read only confirmed batches and immutable snapshot metadata. ## Concrete minimal change plan 1. **New deterministic source parser** - Add `src/jarvis_finance/imports/truewealth_snapshot.py`. - Parse approved bytes in memory, calculate content hash, exact valuation date and CHF total, enforce bounds/format, and expose no raw content/filename. 2. **Extend existing ingestion service** - Modify `src/jarvis_finance/services/portfolio_data.py`. - Add `truewealth_verified_snapshot` to `SOURCE_KEYS`. - Add `_truewealth_document()`, `_truewealth_account()`, `_truewealth_records()`, and a source-specific insert branch. - Write canonical `portfolio_valuation_snapshots` plus immutable ingestion batch/items/audit. - File-hash replay protection must mirror PostFinance. - Fail closed on ambiguous account, date, total, malformed document, or blocked row. - Do not create instruments, transactions, `positions_snapshot`, or cash rows. 3. **Extend strict API schemas** - Modify `src/jarvis_finance/api/schemas/portfolio_data.py`. - Add the source literal and source-specific bounded payload fields. - Existing overview router can remain unchanged. 4. **Read canonical verified value without double count** - Modify `src/jarvis_finance/services/portfolio_service.py::_latest_truewealth_value`. - Prefer latest confirmed immutable `portfolio_valuation_snapshots` source for the TrueWealth account; retain manual `account_value_snapshots` as backward-compatible fallback. 5. **Optional/likely migration 44** - Modify `src/jarvis_finance/storage/migrations.py`. - Use `044_truewealth_verified_snapshot_import_v1`. - Only needed if adding source-specific provenance/immutability to the legacy display table; the canonical valuation and ingestion tables already exist. - Add v44 to `_apply_compat_migrations()` and update version assertions. 6. **Focused synthetic tests** - Add `tests/unit/test_truewealth_verified_snapshot_import.py`. - Cover: - preview causes zero DB changes; - raw content/filename absent from response/history/audit; - confirm atomicity and audit; - exact file replay and concurrent confirmation idempotency; - changed payload/source revision rejection; - expired preview; - malformed/ambiguous files write nothing; - immutable/versioned correction; - manual snapshot backward compatibility; - TrueWealth aggregate counted exactly once; - PostFinance rows and totals unchanged; - GET overview/history has zero DB changes. - Update `tests/unit/test_schema.py` and the migration assertion in `test_portfolio_data_ingestion_reconciliation.py` if v44 is introduced. ## Issues encountered - No repository or tooling blockers. - One expected lookup found no separate `api/routers/portfolio_data.py`; portfolio ingestion is implemented in `api/routers/overview.py`. - The legacy broker mapping document refers to migration `003_broker_bank_mapping_foundation`, while the current cumulative migration implementation is now at version 43; treat that document’s numbering as historical.