# Review guidelines:

You are acting as a reviewer for a proposed code change made by another engineer.

Below are some default guidelines for determining whether the original author would appreciate the issue being flagged.

These are not the final word in determining whether an issue is a bug. In many cases, you will encounter other, more specific guidelines. These may be present elsewhere in a developer message, a user message, a file, or even elsewhere in this system message.
Those guidelines should be considered to override these general instructions.

Here are the general guidelines for determining whether something is a bug and should be flagged.

1. It meaningfully impacts the accuracy, performance, security, or maintainability of the code.
2. The bug is discrete and actionable (i.e. not a general issue with the codebase or a combination of multiple issues).
3. Fixing the bug does not demand a level of rigor that is not present in the rest of the codebase (e.g. one doesn't need very detailed comments and input validation in a repository of one-off scripts in personal projects)
4. The bug was introduced in the commit (pre-existing bugs should not be flagged).
5. The author of the original PR would likely fix the issue if they were made aware of it.
6. The bug does not rely on unstated assumptions about the codebase or author's intent.
7. It is not enough to speculate that a change may disrupt another part of the codebase, to be considered a bug, one must identify the other parts of the code that are provably affected.
8. The bug is clearly not just an intentional change by the original author.

When flagging a bug, you will also provide an accompanying comment. Once again, these guidelines are not the final word on how to construct a comment -- defer to any subsequent guidelines that you encounter.

1. The comment should be clear about why the issue is a bug.
2. The comment should appropriately communicate the severity of the issue. It should not claim that an issue is more severe than it actually is.
3. The comment should be brief. The body should be at most 1 paragraph. It should not introduce line breaks within the natural language flow unless it is necessary for the code fragment.
4. The comment should not include any chunks of code longer than 3 lines. Any code chunks should be wrapped in markdown inline code tags or a code block.
5. The comment should clearly and explicitly communicate the scenarios, environments, or inputs that are necessary for the bug to arise. The comment should immediately indicate that the issue's severity depends on these factors.
6. The comment's tone should be matter-of-fact and not accusatory or overly positive. It should read as a helpful AI assistant suggestion without sounding too much like a human reviewer.
7. The comment should be written such that the original author can immediately grasp the idea without close reading.
8. The comment should avoid excessive flattery and comments that are not helpful to the original author. The comment should avoid phrasing like "Great job ...", "Thanks for ...".

Below are some more detailed guidelines that you should apply to this specific review.

HOW MANY FINDINGS TO RETURN:

Output all findings that the original author would fix if they knew about it. If there is no finding that a person would definitely love to see and fix, prefer outputting no findings. Do not stop at the first qualifying finding. Continue until you've listed every qualifying finding.

GUIDELINES:

- Ignore trivial style unless it obscures meaning or violates documented standards.
- Use one comment per distinct issue (or a multi-line range if necessary).
- Use ```suggestion blocks ONLY for concrete replacement code (minimal lines; no commentary inside the block).
- In every ```suggestion block, preserve the exact leading whitespace of the replaced lines (spaces vs tabs, number of spaces).
- Do NOT introduce or remove outer indentation levels unless that is the actual fix.

The comments will be presented in the code review as inline comments. You should avoid providing unnecessary location details in the comment body. Always keep the line range as short as possible for interpreting the issue. Avoid ranges longer than 5–10 lines; instead, choose the most suitable subrange that pinpoints the problem.

At the beginning of the finding title, tag the bug with priority level. For example "[P1] Un-padding slices along wrong tensor dimensions". [P0] – Drop everything to fix.  Blocking release, operations, or major usage. Only use for universal issues that do not depend on any assumptions about the inputs. · [P1] – Urgent. Should be addressed in the next cycle · [P2] – Normal. To be fixed eventually · [P3] – Low. Nice to have.

Additionally, include a numeric priority field in the JSON output for each finding: set "priority" to 0 for P0, 1 for P1, 2 for P2, or 3 for P3. If a priority cannot be determined, omit the field or use null.

At the end of your findings, output an "overall correctness" verdict of whether or not the patch should be considered "correct".
Correct implies that existing code and tests will not break, and the patch is free of bugs and other blocking issues.
Ignore non-blocking issues such as style, formatting, typos, documentation, and other nits.

FORMATTING GUIDELINES:
The finding description should be one paragraph.

OUTPUT FORMAT:

## Output schema  — MUST MATCH *exactly*

```json
{
  "findings": [
    {
      "title": "<≤ 80 chars, imperative>",
      "body": "<valid Markdown explaining *why* this is a problem; cite files/lines/functions>",
      "confidence_score": <float 0.0-1.0>,
      "priority": <int 0-3, optional>,
      "code_location": {
        "absolute_file_path": "<file path>",
        "line_range": {"start": <int>, "end": <int>}
      }
    }
  ],
  "overall_correctness": "patch is correct" | "patch is incorrect",
  "overall_explanation": "<1-3 sentence explanation justifying the overall_correctness verdict>",
  "overall_confidence_score": <float 0.0-1.0>
}
```

* **Do not** wrap the JSON in markdown fences or extra prose.
* The code_location field is required and must include absolute_file_path and line_range.
* Line ranges must be as short as possible for interpreting the issue (avoid ranges over 5–10 lines; pick the most suitable subrange).
* The code_location should overlap with the diff.
* Do not generate a PR fix.


<permissions instructions>
Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `read-only`: The sandbox only permits reading files. Network access is restricted.
Approval policy is currently never. Do not provide the `sandbox_permissions` for any reason, commands will be rejected.
</permissions instructions>

<apps_instructions>
## Apps (Connectors)
Apps (Connectors) can be explicitly triggered in user messages in the format `[$app-name](app://{connector_id})`. Apps can also be implicitly triggered as long as the context suggests usage of available apps.
An app is equivalent to a set of MCP tools within the `codex_apps` MCP.
An installed app's MCP tools are either provided to you already, or can be lazy-loaded through the `tool_search` tool. If `tool_search` is available, the apps that are searchable by `tools_search` will be listed by it.
Do not additionally call list_mcp_resources or list_mcp_resource_templates for apps.
</apps_instructions>

<skills_instructions>
## Skills
A skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list of skills that can be used. Each entry includes a name, description, and file path so you can open the source for full instructions when using a specific skill.
### Available skills
- imagegen: Generate or edit raster images when the task benefits from AI-created bitmap visuals such as photos, illustrations, textures, sprites, mockups, or transparent-background cutouts. Use when Codex should create a brand-new image, transform an existing image, or derive visual variants from references, and the output should be a bitmap asset rather than repo-native code or vector. Do not use when the task is better handled by editing existing SVG/vector/code-native assets, extending an established icon or logo system, or building the visual directly in HTML/CSS/canvas. (file: /home/agent/.codex/skills/.system/imagegen/SKILL.md)
- openai-docs: Use when the user asks how to build with OpenAI products or APIs, asks about Codex itself or choosing Codex surfaces, needs up-to-date official documentation with citations, help choosing the latest model for a use case, or model upgrade and prompt-upgrade guidance; use OpenAI docs MCP tools for non-Codex docs questions, use the Codex manual helper first for broad Codex self-knowledge, and restrict fallback browsing to official OpenAI domains. (file: /home/agent/.codex/skills/.system/openai-docs/SKILL.md)
- plugin-creator: Create and scaffold plugin directories for Codex with a required `.codex-plugin/plugin.json`, optional plugin folders/files, valid manifest defaults, and personal-marketplace entries by default. Use when Codex needs to create a new personal plugin, add optional plugin structure, generate or update marketplace entries for plugin ordering and availability metadata, or update an existing local plugin during development with the CLI-driven cachebuster and reinstall flow. (file: /home/agent/.codex/skills/.system/plugin-creator/SKILL.md)
- skill-creator: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Codex's capabilities with specialized knowledge, workflows, or tool integrations. (file: /home/agent/.codex/skills/.system/skill-creator/SKILL.md)
- skill-installer: Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos). (file: /home/agent/.codex/skills/.system/skill-installer/SKILL.md)
- data-analytics:analyze-data-quality: Assess whether structured data, query results, dashboards, or analytical evidence are trustworthy enough to use. Use when the task is to check data quality, reconcile conflicting sources or metric definitions, or decide whether evidence is safe to cite. (file: /home/agent/.codex/plugins/cache/openai-curated-remote/data-analytics/0.2.8-13ceeea1f599/skills/analyze-data-quality/SKILL.md)
- data-analytics:build-dashboard: Build source-backed dashboards for monitoring performance, exploring drivers, or acting on product and business metrics. Use when the task needs a dashboard, scorecard, or monitoring view with clear metrics, filters, source definitions, and QA. (file: /home/agent/.codex/plugins/cache/openai-curated-remote/data-analytics/0.2.8-13ceeea1f599/skills/build-dashboard/SKILL.md)
- data-analytics:build-report: Build polished analytical reports for executive, product, business, or technical audiences. Use when the task needs a durable answer-first narrative with evidence-backed findings, visuals or tables, caveats, and source context. (file: /home/agent/.codex/plugins/cache/openai-curated-remote/data-analytics/0.2.8-13ceeea1f599/skills/build-report/SKILL.md)
- data-analytics:create-data-context: Create, update, inspect, or repair Data Analytics semantic layers. Use when the user asks to save data context or create a semantic layer that future Data Analytics work can inspect and cite. (file: /home/agent/.codex/plugins/cache/openai-curated-remote/data-analytics/0.2.8-13ceeea1f599/skills/create-data-context/SKILL.md)
- data-analytics:design-kpis: Design KPI frameworks, metric definitions, targets, guardrails, and measurement plans for product or business decisions. Use when success metrics, drivers, guardrails, targets, or the measurement approach need to be defined or improved. (file: /home/agent/.codex/plugins/cache/openai-curated-remote/data-analytics/0.2.8-13ceeea1f599/skills/design-kpis/SKILL.md)
- data-analytics:gather-business-context: Gather business context from connected or provided sources so downstream analysis starts with the right framing. Use when an analytical question depends on missing context, such as what a metric means, what changed recently, or which sources should be checked. If the same prompt asks for diagnosis, recommendation, or a deliverable, gather context first and continue to the focused skill. (file: /home/agent/.codex/plugins/cache/openai-curated-remote/data-analytics/0.2.8-13ceeea1f599/skills/gather-business-context/SKILL.md)
- data-analytics:index: Route Data Analytics plugin-level requests and broad analytics work to the right focused workflow. Use when Data Analytics is at-mentioned, or for analytics requests involving data, metrics, dashboards, reports, charts, notebooks, spreadsheets, KPIs, market sizing, or semantic layers. (file: /home/agent/.codex/plugins/cache/openai-curated-remote/data-analytics/0.2.8-13ceeea1f599/skills/index/SKILL.md)
- data-analytics:jupyter-notebooks: Create, edit, or validate reproducible SQL or Python notebooks. Use for notebooks, SQL/Python scratchpads, reproducible exploration, audit trails, or runnable companions where the analysis should be reviewable or rerunnable. (file: /home/agent/.codex/plugins/cache/openai-curated-remote/data-analytics/0.2.8-13ceeea1f599/skills/jupyter-notebooks/SKILL.md)
- data-analytics:kpi-reporting: Prepare KPI readouts, scorecards, WBR/MBR/QBR updates, and executive summaries from quantitative business or product metrics; use when the task is to report status, compare against targets, explain validated drivers, and state operating implications. (file: /home/agent/.codex/plugins/cache/openai-curated-remote/data-analytics/0.2.8-13ceeea1f599/skills/kpi-reporting/SKILL.md)
- data-analytics:market-sizing: Estimate market, segment, or opportunity size with transparent assumptions and uncertainty. Use for TAM/SAM/SOM, sizing scenarios, or comparing the scale of possible opportunities. (file: /home/agent/.codex/plugins/cache/openai-curated-remote/data-analytics/0.2.8-13ceeea1f599/skills/market-sizing/SKILL.md)
- data-analytics:metric-diagnostics: Diagnose why a metric changed or differs from expectation. Use when the task is to identify likely drivers of a metric movement, anomaly, gap, or discrepancy. (file: /home/agent/.codex/plugins/cache/openai-curated-remote/data-analytics/0.2.8-13ceeea1f599/skills/metric-diagnostics/SKILL.md)
- data-analytics:product-business-analysis: Analyze product or business data to support a decision or recommendation. Use when a decision depends on metric-backed evidence, such as choosing a direction, prioritizing an opportunity, evaluating a change, segmenting users, sizing tradeoffs, or deciding what to do next. (file: /home/agent/.codex/plugins/cache/openai-curated-remote/data-analytics/0.2.8-13ceeea1f599/skills/product-business-analysis/SKILL.md)
- data-analytics:publish-artifact-to-sites: Publish a validated Data Analytics report or dashboard artifact through Sites. Use automatically for durable artifacts in any positively identified Work Mode when the full Sites lifecycle is callable, or in ChatGPT Desktop outside Work Mode only after an explicit Sites request or acceptance of the optional post-handoff sharing offer. (file: /home/agent/.codex/plugins/cache/openai-curated-remote/data-analytics/0.2.8-13ceeea1f599/skills/publish-artifact-to-sites/SKILL.md)
- data-analytics:validate-data: Validate whether an analysis is accurate, well-supported, and ready to share or use for a decision. Use when reviewing methodology, calculations, comparisons, visuals, caveats, or conclusions. (file: /home/agent/.codex/plugins/cache/openai-curated-remote/data-analytics/0.2.8-13ceeea1f599/skills/validate-data/SKILL.md)
- data-analytics:visualize-data: Design, build, revise, or QA quantitative charts and figures. Use when an analytical answer needs visual judgment, whether for an inline answer, report, dashboard, notebook, or artifact. (file: /home/agent/.codex/plugins/cache/openai-curated-remote/data-analytics/0.2.8-13ceeea1f599/skills/visualize-data/SKILL.md)
- github:gh-address-comments: Address actionable GitHub pull request review feedback. Use when the user wants to inspect unresolved review threads, requested changes, or inline review comments on a PR, then implement selected fixes. Use the GitHub app for PR metadata and flat comment reads, and use the bundled GraphQL script via `gh` whenever thread-level state, resolution status, or inline review context matters. (file: /home/agent/.codex/plugins/cache/openai-curated-remote/github/0.1.8-2841cf9749ae/skills/gh-address-comments/SKILL.md)
- github:gh-fix-ci: Use when a user asks to debug or fix failing GitHub PR checks that run in GitHub Actions. Use the GitHub app from this plugin for PR metadata and patch context, and use `gh` for Actions check and log inspection before implementing any approved fix. (file: /home/agent/.codex/plugins/cache/openai-curated-remote/github/0.1.8-2841cf9749ae/skills/gh-fix-ci/SKILL.md)
- github:github: Triage and orient GitHub repository, pull request, and issue work through the connected GitHub app. Use when the user asks for general GitHub help, wants PR or issue summaries, or needs repository context before choosing a more specific GitHub workflow. (file: /home/agent/.codex/plugins/cache/openai-curated-remote/github/0.1.8-2841cf9749ae/skills/github/SKILL.md)
- github:yeet: Publish local changes to GitHub by confirming scope, committing intentionally, pushing the branch, and opening a draft PR through the GitHub app from this plugin, with `gh` used only as a fallback where connector coverage is insufficient. (file: /home/agent/.codex/plugins/cache/openai-curated-remote/github/0.1.8-2841cf9749ae/skills/yeet/SKILL.md)
- gmail:gmail: Manage Gmail inbox triage, mailbox search, thread summaries, action extraction, reply drafting, and email forwarding through connected Gmail data. Use when the user wants to inspect a mailbox or thread, search email with Gmail query syntax, summarize messages, extract decisions and follow-ups, prepare replies or forwarded messages, or organize messages with explicit confirmation before send, archive, delete, or label actions. (file: /home/agent/.codex/plugins/cache/openai-curated-remote/gmail/0.1.5/skills/gmail/SKILL.md)
- gmail:gmail-inbox-triage: Triage a Gmail inbox into actionable buckets such as urgent, needs reply soon, waiting, and FYI using connected Gmail data. Use when the user asks to triage the inbox, rank what needs attention, find what still needs a reply, or separate important mail from noise. (file: /home/agent/.codex/plugins/cache/openai-curated-remote/gmail/0.1.5/skills/gmail-inbox-triage/SKILL.md)
- google-calendar:google-calendar: Manage scheduling and conflicts in connected Google Calendar data. Use when the user wants to inspect calendars, compare availability, review conflicts, find a meeting room, review event notes or attachments, add or adjust reminders, place temporary holds, or draft exact create, update, reschedule, or cancel changes with timezone-aware details. (file: /home/agent/.codex/plugins/cache/openai-curated-remote/google-calendar/1.2.4/skills/google-calendar/SKILL.md)
- google-calendar:google-calendar-daily-brief: Build polished one-day Google Calendar briefs. Use when the user asks for today, tomorrow, or a specific date summary with an agenda, conflict flags, free windows, remaining-meeting readouts, or a calendar brief, and the Google Calendar connector is available. (file: /home/agent/.codex/plugins/cache/openai-curated-remote/google-calendar/1.2.4/skills/google-calendar-daily-brief/SKILL.md)
- google-calendar:google-calendar-free-up-time: Find ways to open up meaningful free time in a connected Google Calendar. Use when the user wants to clear up their day, make room for focus time, create a longer uninterrupted block, or see the smallest set of calendar changes that would give time back. (file: /home/agent/.codex/plugins/cache/openai-curated-remote/google-calendar/1.2.4/skills/google-calendar-free-up-time/SKILL.md)
- google-calendar:google-calendar-group-scheduler: Find and rank good meeting times for multiple people using connected Google Calendar data. Use when the user wants to schedule a group meeting, compare candidate slots across several attendees, find the best compromise time, or add a room check after narrowing the attendee-compatible options. (file: /home/agent/.codex/plugins/cache/openai-curated-remote/google-calendar/1.2.4/skills/google-calendar-group-scheduler/SKILL.md)
- google-calendar:google-calendar-meeting-prep: Build a practical meeting prep brief from a connected Google Calendar event and its nearby context. Use when the user wants to prepare for an upcoming meeting, understand what to read beforehand, pull in linked notes or docs, or get a concise brief on what the meeting appears to require. (file: /home/agent/.codex/plugins/cache/openai-curated-remote/google-calendar/1.2.4/skills/google-calendar-meeting-prep/SKILL.md)
- google-drive:google-docs: Connector-first Google Docs creation and editing in local Codex plugin sessions, with direct native create and batchUpdate workflows for simple docs, DOCX-first import for polished deliverables, target-document checks, smart chip and building-block reconstruction, connector-readback verification, and reference routing for formatting, citations, tables, and write-safety. (file: /home/agent/.codex/plugins/cache/openai-curated-remote/google-drive/0.1.8/skills/google-docs/SKILL.md)
- google-drive:google-drive: Use connected Google Drive as the single entrypoint for Drive, Docs, Sheets, and Slides work. Use when the user wants to find, fetch, organize, share, export, copy, or delete Drive files, or summarize and edit Google Docs, Google Sheets, and Google Slides through one unified Google Drive plugin. (file: /home/agent/.codex/plugins/cache/openai-curated-remote/google-drive/0.1.8/skills/google-drive/SKILL.md)
- google-drive:google-drive-comments: Write, reply to, and resolve Google Drive comments on Docs, Sheets, Slides, and Drive files with evidence-backed location context. Use when the user asks to leave comments, review a file with comments, respond to comment threads, or resolve Drive comments. (file: /home/agent/.codex/plugins/cache/openai-curated-remote/google-drive/0.1.8/skills/google-drive-comments/SKILL.md)
- google-drive:google-sheets: Analyze and edit connected Google Sheets with range precision. Use when the user wants to create Google Sheets, find a spreadsheet, inspect tabs or ranges, search rows, plan formulas, create or repair charts, clean or restructure tables, write concise summaries, or make explicit cell-range updates. (file: /home/agent/.codex/plugins/cache/openai-curated-remote/google-drive/0.1.8/skills/google-sheets/SKILL.md)
- google-drive:google-slides: Google Slides work for finding, reading, summarizing, creating, importing, template following, visual cleanup, source-deck adaptation, structural repair, and content edits in native Slides decks. (file: /home/agent/.codex/plugins/cache/openai-curated-remote/google-drive/0.1.8/skills/google-slides/SKILL.md)
- investment-banking:investment-banking: Route Investment Banking workflows for banker-owned transaction, capital markets, valuation, diligence, buyer/investor targeting, pitch, process, and restructuring work. Use when the user is preparing, reviewing, or executing M&A, financing, sponsor, issuer, lender, restructuring, coverage, or deal-advisory work, including CIMs, teasers, pitch materials, buyer lists, process trackers, merger models, capital markets analysis, and banker-facing valuation or diligence outputs. Do not use for public-equity investment decisions, personal financial advice, legal advice, FP&A, or generic writing tasks with no transaction or banker-workflow context. (file: /home/agent/.codex/plugins/cache/openai-curated-remote/investment-banking/0.1.29/skills/investment-banking/SKILL.md)
- public-equity-investing:public-equity-investing: Route Public Equity Investing workflows for listed-company and public-security research, earnings analysis, valuation, model updates, long/short pitches, catalysts, thesis tracking, ETF/index and constituent diligence, position sizing, hedging, dashboards, and investment memos. Use when the user is evaluating a public company, ticker, sector, portfolio position, event, earnings setup, or investment thesis through an investor lens, including what is priced in, what is mispriced, what proves or kills the thesis, and what to watch next. Do not use for private-company diligence, credit-first analysis, personal financial advice, generic company summaries, or generic document drafting with no public-equity investment context. (file: /home/agent/.codex/plugins/cache/openai-curated-remote/public-equity-investing/0.1.31/skills/public-equity-investing/SKILL.md)
### How to use skills
- Discovery: The list above is the skills available in this session (name + description + file path). Skill bodies live on disk at the listed paths.
- Trigger rules: If the user names a skill (with `$SkillName` or plain text) OR the task clearly matches a skill's description shown above, you must use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned.
- Missing/blocked: If a named skill isn't in the list or the path can't be read, say so briefly and continue with the best fallback.
- How to use a skill (progressive disclosure):
  1) After deciding to use a skill, the main agent must open and read its `SKILL.md` completely before taking task actions. If a read is truncated or paginated, continue until EOF.
  2) When `SKILL.md` references relative paths (e.g., `scripts/foo.py`), resolve them relative to the skill directory listed above first, and only consider other paths if needed.
  3) If `SKILL.md` points to extra folders such as `references/`, use its routing instructions to identify the files required for the task. The main agent must read each required instruction or reference file itself before acting on it. Do not delegate reading, summarizing, or interpreting skill instructions to a subagent. Subagents may still perform task work when the selected skill allows it.
  4) If `scripts/` exist, prefer running or patching them instead of retyping large code blocks.
  5) If `assets/` or templates exist, reuse them instead of recreating from scratch.
- Coordination and sequencing:
  - If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them.
  - Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why.
- Context hygiene:
  - Progressive disclosure applies to selecting relevant files, not partially reading a selected instruction file. Do not load unrelated references, scripts, or assets.
  - Avoid deep reference-chasing: prefer opening only files directly linked from `SKILL.md` unless you're blocked.
  - When variants exist (frameworks, providers, domains), pick only the relevant reference file(s) and note that choice.
- Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue.
</skills_instructions>

<plugins_instructions>
## Plugins
A plugin is a local bundle of skills, MCP servers, and apps. Below is the list of plugins that are enabled and available in this session.
### Available plugins
- `Data Analytics`: Answer product and business questions with data
- `GitHub`: Inspect repositories, triage pull requests and issues, debug CI, and publish changes through a hybrid GitHub connector and CLI workflow.
- `Gmail`: Work with Gmail using the configured Gmail app connector.
- `Google Calendar`: Connect Google Calendar for scheduling, availability, daily briefs, and event management.
- `Google Drive`: Use Google Drive as the single entrypoint for Drive, Docs, Sheets, and Slides work.
- `Investment Banking`: Investment banking workflows for M&A, coverage, sponsors, capital markets, LevFin, restructuring, valuation, diligence, pitch materials, and deal execution.
- `Lovable`: Build apps and websites
- `Default templates`: OpenAI default document, presentation, and spreadsheet templates.
- `Public Equity Investing`: Public equity investing workflows for listed-company research, earnings analysis, valuation, model updates, long/short pitches, catalysts, thesis tracking, ETF/index and constituent diligence, sell-side research notes, risk sizing, hedging, dashboards, and investment memos.
### How to use plugins
- Discovery: The list above is the plugins available in this session.
- Skill naming: If a plugin contributes skills, those skill entries are prefixed with `plugin_name:` in the Skills list.
- Trigger rules: If the user explicitly names a plugin, prefer capabilities associated with that plugin for that turn.
- Relationship to capabilities: Plugins are not invoked directly. Use their underlying skills, MCP tools, and app tools to help solve the task.
- Preference: When a relevant plugin is available, prefer using capabilities associated with that plugin over standalone capabilities that provide similar functionality.
- Missing/blocked: If the user requests a plugin that is not listed above, or the plugin does not have relevant callable capabilities for the task, say so briefly and continue with the best fallback.
</plugins_instructions>


# AGENTS.md instructions for /home/agent/.hermes/worktrees/FinanceManager-sprint12-truewealth-verified-import

<INSTRUCTIONS>
# AGENTS.md

## FinanceManager-2.0-Regeln

- Arbeite in kleinen vertikalen PRs; keine Big-Bang-Rewrites.
- Produktive Finanzdaten, Rohimporte, Datenbanken, Reports und Secrets bleiben außerhalb Git.
- Schreibrouten bleiben Preview → Confirm → Audit und HTTP-seitig fail-closed. Keine Orderausführung.
- Sol entscheidet über Architektur, Security, Auth, Datenmodell, Migrationen, Formeln und Advisor-/IPS-Semantik.
- Spark nur sequenziell für ein kleines Ergebnis (ca. drei Dateien), nie für Secrets, produktive Daten, Auth, Migrationen oder Finanzberechnungen; Sol prüft jeden Diff.
- Kein Push, Merge, Deployment und keine Secret-Rotation ohne ausdrückliche Freigabe.
- Vor Abschluss: `make PYTHON=.venv/bin/python verify` und `git diff --check` tatsächlich ausführen.
- Jeder PR erfüllt die Felder der PR-Vorlage und aktualisiert bei Entscheidungsänderungen die Baseline/ADR.

Details: `docs/finance-manager-2.0/sprint-0-baseline.md`.

</INSTRUCTIONS>

<environment_context>
  <cwd>/home/agent/.hermes/worktrees/FinanceManager-sprint12-truewealth-verified-import</cwd>
  <shell>bash</shell>
  <current_date>2026-07-27</current_date>
  <timezone>Europe/Zurich</timezone>
  <filesystem><workspace_roots><root>/home/agent/.hermes/worktrees/FinanceManager-sprint12-truewealth-verified-import</root></workspace_roots><permission_profile type="managed"><file_system type="restricted"><entry access="read"><special>:root</special></entry></file_system></permission_profile></filesystem>
</environment_context>


Summary: auto


Review the current code changes (staged, unstaged, and untracked files) and provide prioritized findings.


git status --short && git diff --stat && git diff --cached --stat


Output:
Chunk ID: 010402
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 732
Output:
 M .github/workflows/portfolio-phase3-integration.yml
 M docs/sprint12-truewealth-source-gate.md
 M docs/sprint12-truewealth-verified-import.md
 M frontend/src/pages/CashTrueWealthManagement.test.ts
 M frontend/src/pages/TrueWealthPage.vue
 M pyproject.toml
 M requirements-ci.txt
 M scripts/ci_portfolio_phase3_gate.py
 M src/jarvis_finance/api/main.py
 M src/jarvis_finance/services/portfolio_data.py
 M src/jarvis_finance/services/portfolio_performance.py
 M src/jarvis_finance/services/portfolio_service.py
 M src/jarvis_finance/services/reconciliation_snapshot.py
 M src/jarvis_finance/storage/migrations.py
 M tests/unit/test_budget_monthly_import_rule_learning_v1.py
 M tests/unit/test_budget_phase1.py
 M tests/unit/test_budget_phase11.py
 M tests/unit/test_budget_phase12_seed_review.py
 M tests/unit/test_budget_phase16_user_rules.py
 M tests/unit/test_fixed_costs_subscriptions_v1.py
 M tests/unit/test_grocery_matching_learning_v2.py
 M tests/unit/test_grocery_price_providers_v1.py
 M tests/unit/test_portfolio_data_ingestion_reconciliation.py
 M tests/unit/test_schema.py
?? frontend/src/api/truewealth.ts
?? src/jarvis_finance/api/routers/truewealth.py
?? src/jarvis_finance/api/schemas/truewealth.py
?? src/jarvis_finance/imports/truewealth_tax_statement.py
?? src/jarvis_finance/services/truewealth_service.py
?? tests/test_truewealth_sprint12.py
 .github/workflows/portfolio-phase3-integration.yml |   5 +
 docs/sprint12-truewealth-source-gate.md            |  95 +++++-----
 docs/sprint12-truewealth-verified-import.md        |  52 ++++--
 .../src/pages/CashTrueWealthManagement.test.ts     |  30 +--
 frontend/src/pages/TrueWealthPage.vue              | 208 +++++++++++++++++++--
 pyproject.toml                                     |   2 +-
 requirements-ci.txt                                |   1 +
 scripts/ci_portfolio_phase3_gate.py                |   6 +-
 src/jarvis_finance/api/main.py                     |  11 +-
 src/jarvis_finance/services/portfolio_data.py      |   7 +-
 .../services/portfolio_performance.py              |   6 +-
 src/jarvis_finance/services/portfolio_service.py   |   5 +-
 .../services/reconciliation_snapshot.py            |   5 +-
 src/jarvis_finance/storage/migrations.py           | 172 ++++++++++++++++-
 .../test_budget_monthly_import_rule_learning_v1.py |   2 +-
 tests/unit/test_budget_phase1.py                   |   2 +-
 tests/unit/test_budget_phase11.py                  |   2 +-
 tests/unit/test_budget_phase12_seed_review.py      |   2 +-
 tests/unit/test_budget_phase16_user_rules.py       |   2 +-
 tests/unit/test_fixed_costs_subscriptions_v1.py    |   2 +-
 tests/unit/test_grocery_matching_learning_v2.py    |   2 +-
 tests/unit/test_grocery_price_providers_v1.py      |   2 +-
 ...test_portfolio_data_ingestion_reconciliation.py |   4 +-
 tests/unit/test_schema.py                          |   2 +-
 24 files changed, 509 insertions(+), 118 deletions(-)


Output:
Chunk ID: 91a9a3
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 14184
Output:
Total output lines: 809

diff --git a/.github/workflows/portfolio-phase3-integration.yml b/.github/workflows/portfolio-phase3-integration.yml
index 7b317f6..32341ae 100644
--- a/.github/workflows/portfolio-phase3-integration.yml
+++ b/.github/workflows/portfolio-phase3-integration.yml
@@ -100,7 +100,9 @@ jobs:
             scripts/ci_portfolio_phase3_gate.py \
             src/jarvis_finance/api/main.py \
             src/jarvis_finance/api/routers/overview.py \
+            src/jarvis_finance/api/routers/truewealth.py \
             src/jarvis_finance/api/schemas/portfolio_data.py \
+            src/jarvis_finance/api/schemas/truewealth.py \
             src/jarvis_finance/api/schemas/portfolio_performance.py \
             src/jarvis_finance/api/schemas/portfolio_policy.py \
             src/jarvis_finance/api/schemas/reconciliation.py \
@@ -108,6 +110,7 @@ jobs:
             src/jarvis_finance/config/settings.py \
             src/jarvis_finance/ledger/cost_basis.py \
             src/jarvis_finance/ledger/performance.py \
+            src/jarvis_finance/imports/truewealth_tax_statement.py \
             src/jarvis_finance/quality/freshness.py \
             src/jarvis_finance/quality/git_safety.py \
             src/jarvis_finance/services/portfolio_data.py \
@@ -115,8 +118,10 @@ jobs:
             src/jarvis_finance/services/portfolio_policy.py \
             src/jarvis_finance/services/reconciliation_snapshot.py \
             src/jarvis_finance/services/transfer_pairing.py \
+            src/jarvis_finance/services/truewealth_service.py \
             src/jarvis_finance/storage/migrations.py \
             tests/conftest.py \
+            tests/test_truewealth_sprint12.py \
             tests/unit/test_api_write_security.py \
             tests/unit/test_git_safety.py \
             tests/unit/test_portfolio_data_ingestion_reconciliation.py \
diff --git a/frontend/src/pages/CashTrueWealthManagement.test.ts b/frontend/src/pages/CashTrueWealthManagement.test.ts
index aec0ab5..f85febe 100644
--- a/frontend/src/pages/CashTrueWealthManagement.test.ts
+++ b/frontend/src/pages/CashTrueWealthManagement.test.ts
@@ -3,13 +3,13 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
 import CashPage from './CashPage.vue'
 import TrueWealthPage from './TrueWealthPage.vue'
 import { getCashSummary, previewCashSnapshot, confirmCashSnapshot, getCashPosition } from '@/api/cash'
-import { getOverview } from '@/api/portfolio'
-import { getReferenceAccounts, previewAccountValue, confirmAccountValue } from '@/api/positions'
+import { confirmManualTrueWealthValue, getTrueWealthSummary, previewManualTrueWealthValue } from '@/api/truewealth'
 import { getHealth, getRuntimeStatus } from '@/api/runtime'

 vi.mock('@/api/cash', () => ({ getCashSummary: vi.fn(), getCashPosition: vi.fn(), previewCashSnapshot: vi.fn(), confirmCashSnapshot: vi.fn() }))
 vi.mock('@/api/portfolio', () => ({ getOverview: vi.fn() }))
 vi.mock('@/api/positions', () => ({ getReferenceAccounts: vi.fn(), previewAccountValue: vi.fn(), confirmAccountValue: vi.fn() }))
+vi.mock('@/api/truewealth', () => ({ getTrueWealthSummary: vi.fn(), previewManualTrueWealthValue: vi.fn(), confirmManualTrueWealthValue: vi.fn(), previewTrueWealthImport: vi.fn(), confirmTrueWealthImport: vi.fn(), setManualTrueWealthValueStatus: vi.fn() }))
 vi.mock('@/api/runtime', () => ({ getHealth: vi.fn(), getRuntimeStatus: vi.fn() }))

 const cashSummary = {
@@ -72,28 +72,32 @@ describe('TrueWealthPage management', () => {
     vi.clearAllMocks()
     vi.mocked(getHealth).mockResolvedValue({ status: 'ok', app: 'jarvis-finance-api', api_version: '0', mode: 'read-only' })
     vi.mocked(getRuntimeStatus).mockResolvedValue({ db_available: true, db_path: '/runtime/finance.sqlite3', base_currency: 'CHF', runtime_outside_repo: true, write_mode: 'preview-confirm' })
-    vi.mocked(getOverview).mockResolvedValue({ total_value_chf: '9000.00', crypto_value_chf: '0.00', equity_value_chf: '0.00', postfinance_equity_value_chf: '0.00', truewealth_value_chf: '9000.00', cash_value_chf: '0.00', unpriced_positions_count: 0, critical_alerts_count: 0, last_price_update: null, data_quality_status: 'ok' })
-    vi.mocked(getReferenceAccounts).mockResolvedValue([{ account_id: 'acct_tw', label: 'TrueWealth · TrueWealth Gesamtwert', account_type: 'managed_portfolio', currency: 'CHF', valuation_mode: 'manual_total_value', portfolio_bucket: 'truewealth', allow_position_adds: false }])
-    vi.mocked(previewAccountValue).mockResolvedValue({ preview_id: 'preview_tw', summary: 'Gesamtwert', amount_chf: '9999.00', fx_status: 'not_needed', warnings: [] })
-    vi.mocked(confirmAccountValue).mockResolvedValue({ status: 'confirmed', entity_id: 'acctval_1', audit_id: 'audit_tw', message: 'gespeichert' })
+    vi.mocked(getTrueWealthSummary).mockResolvedValue({
+      portfolio_label: 'Freie Anlagen', current_value_chf: '12000.00', current_valuation_at: '2026-07-27T23:59:59Z', current_source_type: 'truewealth_official_import', current_is_manual_provisional: false, current_label: 'Gesamtwert gemäss offiziellem Import', positions_snapshot_date: '2026-07-27', positions_label: 'Positionsdetails gemäss letztem Import vom 2026-07-27', mixed_as_of: false, allocation_performance_usable: true,
+      positions: [{ snapshot_position_id: 'hidden_position_id', instrument_name: 'Synthetic ETF', isin: 'CH0000000001', asset_type: null, quantity: '100', price_currency: 'CHF', source_price: '50.00', source_value_chf: '5000.00' }], cash: [], history: [], imports: [], latest_official: {},
+    })
+    vi.mocked(previewManualTrueWealthValue).mockResolvedValue({ preview_id: 'preview_tw', confirmation_id: 'confirm_tw', total_value_chf: '12500.00', valuation_at: '2026-07-28T08:00:00Z', valuation_date: '2026-07-28', note: '', source_type: 'truewealth_manual_provisional', status: 'manual_provisional', warnings: [] })
+    vi.mocked(confirmManualTrueWealthValue).mockResolvedValue({ status: 'confirmed', snapshot_id: 'acctval_1', audit_id: 'audit_tw' })
   })

-  it('shows truewealth manual update workflow and forbids individual positions wording', async () => {
+  it('shows official positions and runs manual provisional preview confirm', async () => {
     const wrapper = mount(TrueWealthPage)
     await flushPromises()

-    expect(wrapper.text()).toContain('aktueller TrueWealth-Gesamtwert CHF')
-    expect(wrapper.text()).toContain('keine Einzelpositionen')
+    expect(wrapper.text()).toContain('Aktueller TrueWealth-Gesamtwert CHF')
+    expect(wrapper.text()).toContain('Synthetic ETF')
+    expect(wrapper.text()).not.toContain('hidden_position_id')
     await wrapper.get('[data-testid="truewealth-update-button"]').trigger('click')
-    await wrapper.get('[data-testid="truewealth-amount-input"]').setValue('9999.00')
-    await wrapper.get('[data-testid="truewealth-date-input"]').setValue('2026-05-21')
+    await wrapper.get('[data-testid="truewealth-amount-input"]').setValue('12500.00')
+    await wrapper.get('[data-testid="truewealth-date-input"]').setValue('2026-07-28T10:00')
     await wrapper.get('[data-testid="truewealth-preview-button"]').trigger('click')
     await flushPromises()
+    expect(wrapper.text()).toContain('Manuell / provisorisch')
     await wrapper.get('[data-testid="truewealth-confirm-button"]').trigger('click')
     await flushPromises()

-    expect(previewAccountValue).toHaveBeenCalledWith('acct_tw', { valuation_date: '2026-05-21', total_value_chf: '9999.00', note: '' })
-    expect(confirmAccountValue).toHaveBeenCalledWith('acct_tw', { valuation_date: '2026-05-21', total_value_chf: '9999.00', note: '', preview_id: 'preview_tw', confirm: true })
+    expect(previewManualTrueWealthValue).toHaveBeenCalledWith({ total_value_chf: '12500.00', valuation_at: expect.any(String), note: '' })
+    expect(confirmManualTrueWealthValue).toHaveBeenCalledWith({ total_value_chf: '12500.00', valuation_at: expect.any(String), note: '', preview_id: 'preview_tw', confirmation_id: 'confirm_tw', confirm: true })
     expect(wrapper.text()).toContain('Audit')
   })
 })
diff --git a/frontend/src/pages/TrueWealthPage.vue b/frontend/src/pages/TrueWealthPage.vue
index 0e8af9b..657ad9c 100644
--- a/frontend/src/pages/TrueWealthPage.vue
+++ b/frontend/src/pages/TrueWealthPage.vue
@@ -1,29 +1,205 @@
 <template>

-    <LoadActionBar testid="refresh-truewealth" title="TrueWealth Roboadvisor" description="Managed Portfolio als eigener Anlagenbereich. Keine Einzelinstrument-Sammlung." :loading="loading" @load="loadData(true)" />
+    <LoadActionBar
+      testid="refresh-truewealth"
+      title="TrueWealth · Freie Anlagen"
+      description="Offizielle Portfolio-Snapshots und provisorische manuelle Gesamtwerte mit klar getrenntem Datenstand."
+      :loading="loading"
+      @load="loadData(true)"
+    />
     <LocalDataStatus :backend-ok="backendOk" :runtime-ok="runtimeOk" :loading="loading" :error="error" :last-loaded-at="lastLoadedAt" />
-

+
+

-

Anlagen

aktueller TrueWealth-Gesamtwert CHF

Manuell gepflegter Gesamtwert. keine Einzelpositionen, keine Aktien-/ETF-Zuordnung, kein PostFinance-Mix.

-        <MoneyValue :value="overview?.truewealth_value_chf ?? '0.00'" />
+

+
Freie Anlagen

+
Aktueller TrueWealth-Gesamtwert CHF

+
{{ summary?.current_label ?? 'Noch kein Wert verfügbar' }}

+

+        <MoneyValue :value="summary?.current_value_chf ?? '0.00'" />
+

+

+        Manuell / provisorisch. {{ summary.positions_label }}. Die Differenz wird weder Positionen noch Cash zugerechnet.
+

+

+        Gesamtwert und Positionsdetails haben unterschiedliche Stichtage. Allokation, Gewinn und Rendite sind für diesen Mischstand deaktiviert.
+

+

+        <button data-testid="truewealth-update-button" type="button" class="rounded-lg border border-cyan-300 bg-cyan-50 px-3 py-2 font-semibold text-cyan-800" @click="showManualForm = !showManualForm">Gesamtwert manuell aktualisieren</button>
+        <button data-testid="truewealth-import-button" type="button" class="rounded-lg border border-slate-300 bg-white px-3 py-2 font-semibold text-slate-800" @click="showImportForm = !showImportForm">Offiziellen Auszug importieren</button>
+        Zur Portfolio-Übersicht

-
<button data-testid="truewealth-update-button" type="button" class="rounded-lg border border-cyan-300 bg-cyan-50 px-3 py-2 font-semibold text-cyan-800" @click="showForm = true">TrueWealth-Wert aktualisieren</button>Zur Portfolio-Übersicht

-
Keine Provider-Abfrage beim Rendern. Keine Positionen unter TrueWealth erfassen.

-

TrueWealth-Wert aktualisieren

<input data-testid="truewealth-amount-input" v-model="form.total_value_chf" class="rounded border p-2" placeholder="Gesamtwert CHF" /><input data-testid="truewealth-date-input" v-model="form.valuation_date" class="rounded border p-2" type="date" /><input data-testid="truewealth-note-input" v-model="form.note" class="rounded border p-2" placeholder="Notiz optional" />

<button data-testid="truewealth-preview-button" class="rounded bg-slate-900 px-3 py-2 text-white" @click="doPreview">Preview</button><button v-if="preview" data-testid="truewealth-confirm-button" class="rounded bg-emerald-600 px-3 py-2 text-white" @click="doConfirm">Confirm</button>

Preview: {{ preview.amount_chf }}

{{ message }}

+

+
Gesamtwert manuell aktualisieren

+
Erstellt einen neuen historischen, provisorischen Datensatz. Offizielle Positionen bleiben unverändert.

+

+        Gesamtwert CHF<input data-testid="truewealth-amount-input" v-model="manualForm.total_value_chf" class="mt-1 w-full rounded-lg border border-slate-300 bg-white p-2" inputmode="decimal" placeholder="Betrag in CHF" @input="manualPreview = null" />
+        Bewertungszeitpunkt<input data-testid="truewealth-date-input" v-model="manualForm.valuation_at" class="mt-1 w-full rounded-lg border border-slate-300 bg-white p-2" type="datetime-local" @input="manualPreview = null" />
+        Notiz optional<input data-testid="truewealth-note-input" v-model="manualForm.note" class="mt-1 w-full rounded-lg border border-slate-300 bg-white p-2" maxlength="240" placeholder="z. B. Stand im Kundenportal" @input="manualPreview = null" />
+

+

+        <button data-testid="truewealth-preview-button" class="rounded-lg bg-slate-900 px-3 py-2 font-semibold text-white disabled:opacity-50" :disabled="actionLoading || !manualForm.total_value_chf || !manualForm.valuation_at" @click="doManualPreview">Preview</button>
+        <button v-if="manualPreview" data-testid="truewealth-confirm-button" class="rounded-lg bg-emerald-600 px-3 py-2 font-semibold text-white disabled:opacity-50" :disabled="actionLoading" @click="doManualConfirm">Confirm</button>
+

+

+
Preview: CHF {{ money(manualPreview.total_value_chf) }} · {{ formatDateTime(manualPreview.valuation_at) }}

+
Manuell / provisorisch. Positionsdetails werden nicht verändert.

+

+

-

Verlauf der manuellen Gesamtwert-Snapshots

Historie wird aus bestätigten Snapshots geführt; Audit-Hinweis je Confirm vorhanden. Keine Roh-IDs im User Mode.

+

+
Offiziellen TrueWealth-Auszug importieren

+
Nur unveränderte TrueWealth-Steuerauszüge als PDF. Der Import wird zuerst vollständig geprüft und angezeigt.

+      <input data-testid="truewealth-file-input" class="mt-3 block w-full rounded-lg border border-slate-300 bg-white p-2 text-sm" type="file" accept="application/pdf,.pdf" @change="onFileSelected" />
+

+        <button data-testid="truewealth-import-preview-button" class="rounded-lg bg-slate-900 px-3 py-2 font-semibold text-white disabled:opacity-50" :disabled="actionLoading || !importPayload" @click="doImportPreview">Import-Preview</button>
+        <button v-if="importPreview" data-testid="truewealth-import-confirm-button" class="rounded-lg bg-emerald-600 px-3 py-2 font-semibold text-white disabled:opacity-50" :disabled="actionLoading || importPreview.reconciliation_status === 'mismatch' || importPreview.conflict" @click="doImportConfirm">Import bestätigen</button>
+

+

+

Stichtag

{{ formatDate(importPreview.snapshot_date) }}

+

Gesamtwert

CHF {{ money(importPreview.source_total_chf) }}

+

Inhalt

{{ importPreview.positions.length }} Positionen · {{ importPreview.cash.length }} Cashkonten

+

Reconciliation

{{ importPreview.reconciliation_status }} · CHF {{ money(importPreview.reconciliation_difference_chf) }}

+
Identische Quelle bereits bestätigt. Ein erneuter Confirm ist idempotent.

+
Konflikt: Für diesen Stichtag existiert bereits eine andere offizielle Quelle. Confirm ist gesperrt.

+

+

+
+
{{ message }}

+
{{ actionError }}

+
+

+

Positionsdetails

{{ summary?.positions_label }}

{{ summary?.positions.length ?? 0 }} Positionen

+

+

+
{{ position.instrument_name }}

{{ position.isin }}

+
Menge
{{ position.quantity }}Quellwert
CHF {{ money(position.source_value_chf) }}

+

+

+
Noch keine offiziellen Positionen importiert.

+      <div v-if="summary?.cash.length"…4184 tokens truncated…              ) AS rn
           FROM account_value_snapshots s
           WHERE account_id IN ({placeholders})
             AND valuation_date BETWEEN ? AND ?
             AND created_at <= ?
-            AND updated_at IS NULL
+            AND COALESCE(is_active,1)=1
+            AND source_type <> 'truewealth_manual_provisional'
         )
         SELECT * FROM ranked WHERE rn=1 ORDER BY valuation_date, account_id
         """,
diff --git a/src/jarvis_finance/services/portfolio_service.py b/src/jarvis_finance/services/portfolio_service.py
index 74344d1..b270e8c 100644
--- a/src/jarvis_finance/services/portfolio_service.py
+++ b/src/jarvis_finance/services/portfolio_service.py
@@ -18,8 +18,11 @@ def _latest_truewealth_value(conn: Connection) -> Decimal:
         JOIN accounts a ON a.account_id = avs.account_id
         JOIN platforms p ON p.platform_id = a.platform_id
         WHERE a.is_active = 1
+          AND COALESCE(avs.is_active,1) = 1
           AND (lower(p.name || ' ' || a.account_name) LIKE '%true%wealth%' OR lower(a.account_type) IN ('managed_portfolio','robo_portfolio'))
-        ORDER BY avs.valuation_date DESC, avs.created_at DESC
+        ORDER BY avs.valuation_date DESC,
+                 CASE avs.source_type WHEN 'truewealth_official_import' THEN 3 WHEN 'truewealth_manual_provisional' THEN 2 ELSE 1 END DESC,
+                 COALESCE(avs.valuation_at,avs.created_at) DESC, avs.created_at DESC
         LIMIT 1
         """
     ).fetchone()
diff --git a/src/jarvis_finance/services/reconciliation_snapshot.py b/src/jarvis_finance/services/reconciliation_snapshot.py
index 086b2cb..8a76ece 100644
--- a/src/jarvis_finance/services/reconciliation_snapshot.py
+++ b/src/jarvis_finance/services/reconciliation_snapshot.py
@@ -201,7 +201,10 @@ def list_snapshot_metadata(conn: Connection, *, now: datetime | None = None) ->
         conn,
         """SELECT account_id, valuation_date, created_at, source_type, currency, total_value_chf, quality_status
            FROM account_value_snapshots
-           ORDER BY account_id, valuation_date DESC, created_at DESC, snapshot_id DESC""",
+           WHERE COALESCE(is_active,1)=1
+           ORDER BY account_id, valuation_date DESC,
+                    CASE source_type WHEN 'truewealth_official_import' THEN 3 WHEN 'truewealth_manual_provisional' THEN 2 ELSE 1 END DESC,
+                    COALESCE(valuation_at,created_at) DESC, created_at DESC, snapshot_id DESC""",
     )
     latest_accounts: set[str] = set()
     for row in valuations:
diff --git a/src/jarvis_finance/storage/migrations.py b/src/jarvis_finance/storage/migrations.py
index db02415..41183b7 100644
--- a/src/jarvis_finance/storage/migrations.py
+++ b/src/jarvis_finance/storage/migrations.py
@@ -6,8 +6,8 @@ from sqlite3 import Connection

 from .schema import INITIAL_SCHEMA_SQL

-MIGRATION_VERSION = 43
-MIGRATION_NAME = "043_postfinance_baseline_mapping_audit_v1"
+MIGRATION_VERSION = 44
+MIGRATION_NAME = "044_truewealth_verified_snapshot_v1"

 INSTRUMENT_OPTIONAL_COLUMNS = {
     "position_category": "TEXT",
@@ -1925,6 +1925,173 @@ def _create_daily_market_analytics_tables(conn: Connection) -> None:
     )

+def _create_truewealth_verified_snapshot_v1(conn: Connection) -> None:
+    _add_missing_columns(
+        conn,
+        "account_value_snapshots",
+        {
+            "valuation_at": "TEXT",
+            "source_reference": "TEXT",
+            "is_active": "INTEGER NOT NULL DEFAULT 1",
+            "deactivated_at": "TEXT",
+            "deactivation_reason": "TEXT",
+        },
+    )
+    conn.executescript(
+        """
+        CREATE TABLE IF NOT EXISTS truewealth_portfolios (
+            portfolio_id TEXT PRIMARY KEY,
+            account_id TEXT NOT NULL UNIQUE REFERENCES accounts(account_id),
+            source_reference_hash TEXT NOT NULL,
+            label TEXT NOT NULL,
+            portfolio_kind TEXT CHECK(portfolio_kind IN ('free_assets','pillar_3a','child','other')),
+            base_currency TEXT NOT NULL DEFAULT 'CHF',
+            is_active INTEGER NOT NULL DEFAULT 1 CHECK(is_active IN (0,1)),
+            created_at TEXT NOT NULL,
+            updated_at TEXT
+        );
+
+        CREATE TABLE IF NOT EXISTS truewealth_import_batches (
+            batch_id TEXT PRIMARY KEY,
+            portfolio_id TEXT NOT NULL REFERENCES truewealth_portfolios(portfolio_id),
+            account_id TEXT NOT NULL REFERENCES accounts(account_id),
+            file_sha256 TEXT NOT NULL UNIQUE CHECK(length(file_sha256)=64),
+            filename_sha256 TEXT NOT NULL CHECK(length(filename_sha256)=64),
+            source_file_type TEXT NOT NULL CHECK(source_file_type='application/pdf'),
+            parser_id TEXT NOT NULL,
+            parser_version TEXT NOT NULL,
+            provenance TEXT NOT NULL CHECK(provenance='truewealth_customer_export'),
+            snapshot_date TEXT NOT NULL,
+            page_count INTEGER NOT NULL CHECK(page_count > 0),
+            archive_reference TEXT NOT NULL,
+            status TEXT NOT NULL CHECK(status='confirmed'),
+            audit_id TEXT NOT NULL,
+            confirmed_at TEXT NOT NULL,
+            confirmed_by TEXT NOT NULL DEFAULT 'user'
+        );
+        CREATE INDEX IF NOT EXISTS idx_truewealth_batches_portfolio_date
+            ON truewealth_import_batches(portfolio_id, snapshot_date DESC, confirmed_at DESC);
+
+        CREATE TABLE IF NOT EXISTS truewealth_snapshots (
+            snapshot_id TEXT PRIMARY KEY,
+            portfolio_id TEXT NOT NULL REFERENCES truewealth_portfolios(portfolio_id),
+            account_id TEXT NOT NULL REFERENCES accounts(account_id),
+            batch_id TEXT NOT NULL UNIQUE REFERENCES truewealth_import_batches(batch_id),
+            snapshot_date TEXT NOT NULL,
+            source_total_chf TEXT NOT NULL,
+            securities_total_chf TEXT NOT NULL,
+            cash_total_chf TEXT NOT NULL,
+            components_total_chf TEXT NOT NULL,
+            reconciliation_difference_chf TEXT NOT NULL,
+            reconciliation_tolerance_chf TEXT NOT NULL DEFAULT '1.00',
+            reconciliation_status TEXT NOT NULL CHECK(reconciliation_status IN ('matched','within_tolerance','mismatch')),
+            position_count INTEGER NOT NULL,
+            cash_count INTEGER NOT NULL,
+            completeness_status TEXT NOT NULL CHECK(completeness_status IN ('complete','partial','blocked')),
+            reason_codes_json TEXT NOT NULL DEFAULT '[]'
+                CHECK(json_valid(reason_codes_json) AND json_type(reason_codes_json)='array'),
+            created_at TEXT NOT NULL,
+            UNIQUE(portfolio_id, snapshot_date, batch_id)
+        );
+        CREATE INDEX IF NOT EXISTS idx_truewealth_snapshot_date
+          ON truewealth_snapshots(portfolio_id, snapshot_date DESC);
+        CREATE UNIQUE INDEX IF NOT EXISTS ux_truewealth_snapshot_portfolio_date
+          ON truewealth_snapshots(portfolio_id, snapshot_date);
+        CREATE TABLE IF NOT EXISTS truewealth_snapshot_positions (
+            snapshot_position_id TEXT PRIMARY KEY,
+            snapshot_id TEXT NOT NULL REFERENCES truewealth_snapshots(snapshot_id),
+            source_row_reference TEXT NOT NULL,
+            instrument_name TEXT NOT NULL,
+            isin TEXT NOT NULL,
+            asset_type TEXT,
+            quantity TEXT NOT NULL,
+            price_currency TEXT NOT NULL,
+            source_price TEXT NOT NULL,
+            source_value_chf TEXT NOT NULL,
+            source_evidence_json TEXT NOT NULL DEFAULT '{}'
+                CHECK(json_valid(source_evidence_json) AND json_type(source_evidence_json)='object'),
+            created_at TEXT NOT NULL,
+            UNIQUE(snapshot_id, source_row_reference),
+            UNIQUE(snapshot_id, isin)
+        );
+        CREATE INDEX IF NOT EXISTS idx_truewealth_positions_snapshot
+            ON truewealth_snapshot_positions(snapshot_id, isin);
+
+        CREATE TABLE IF NOT EXISTS truewealth_snapshot_cash (
+            snapshot_cash_id TEXT PRIMARY KEY,
+            snapshot_id TEXT NOT NULL REFERENCES truewealth_snapshots(snapshot_id),
+            source_row_reference TEXT NOT NULL,
+            currency TEXT NOT NULL,
+            amount_original TEXT NOT NULL,
+            fx_rate_to_chf TEXT,
+            source_value_chf TEXT NOT NULL,
+            source_evidence_json TEXT NOT NULL DEFAULT '{}'
+                CHECK(json_valid(source_evidence_json) AND json_type(source_evidence_json)='object'),
+            created_at TEXT NOT NULL,
+            UNIQUE(snapshot_id, source_row_reference),
+            UNIQUE(snapshot_id, currency)
+        );
+        CREATE INDEX IF NOT EXISTS idx_truewealth_cash_snapshot
+            ON truewealth_snapshot_cash(snapshot_id, currency);
+
+        CREATE UNIQUE INDEX IF NOT EXISTS ux_account_value_truewealth_official_reference
+            ON account_value_snapshots(source_reference)
+            WHERE source_type='truewealth_official_import' AND source_reference IS NOT NULL;
+
+        CREATE TRIGGER IF NOT EXISTS truewealth_account_values_no_delete
+        BEFORE DELETE ON account_value_snapshots
+        WHEN OLD.source_type IN ('truewealth_official_import','truewealth_manual_provisional')
+        BEGIN SELECT RAISE(ABORT, 'truewealth account valuations cannot be deleted'); END;
+        CREATE TRIGGER IF NOT EXISTS truewealth_official_account_values_immutable
+        BEFORE UPDATE ON account_value_snapshots
+        WHEN OLD.source_type='truewealth_official_import'
+        BEGIN SELECT RAISE(ABORT, 'official truewealth account valuations are immutable'); END;
+        CREATE TRIGGER IF NOT EXISTS truewealth_manual_value_fields_immutable
+        BEFORE UPDATE ON account_value_snapshots
+        WHEN OLD.source_type='truewealth_manual_provisional' AND (
+            NEW.snapshot_id IS NOT OLD.snapshot_id OR NEW.account_id IS NOT OLD.account_id OR
+            NEW.valuation_date IS NOT OLD.valuation_date OR NEW.total_value_chf IS NOT OLD.total_value_chf OR
+            NEW.currency IS NOT OLD.currency OR NEW.source_type IS NOT OLD.source_type OR
+            NEW.quality_status IS NOT OLD.quality_status OR NEW.notes IS NOT OLD.notes OR
+            NEW.created_at IS NOT OLD.created_at OR NEW.valuation_at IS NOT OLD.valuation_at OR
+            NEW.source_reference IS NOT OLD.source_reference
+        )
+        BEGIN SELECT RAISE(ABORT, 'manual truewealth valuation fields are immutable'); END;
+
+        CREATE TRIGGER IF NOT EXISTS truewealth_batches_immutable_update
+        BEFORE UPDATE ON truewealth_import_batches
+        BEGIN SELECT RAISE(ABORT, 'truewealth import batches are immutable'); END;
+        CREATE TRIGGER IF NOT EXISTS truewealth_batches_no_delete
+        BEFORE DELETE ON truewealth_import_batches
+        BEGIN SELECT RAISE(ABORT, 'truewealth import batches cannot be deleted'); END;
+        CREATE TRIGGER IF NOT EXISTS truewealth_snapshots_immutable_update
+        BEFORE UPDATE ON truewealth_snapshots
+        BEGIN SELECT RAISE(ABORT, 'truewealth snapshots are immutable'); END;
+        CREATE TRIGGER IF NOT EXISTS truewealth_snapshots_no_delete
+        BEFORE DELETE ON truewealth_snapshots
+        BEGIN SELECT RAISE(ABORT, 'truewealth snapshots cannot be deleted'); END;
+        CREATE TRIGGER IF NOT EXISTS truewealth_positions_immutable_update
+        BEFORE UPDATE ON truewealth_snapshot_positions
+        BEGIN SELECT RAISE(ABORT, 'truewealth positions are immutable'); END;
+        CREATE TRIGGER IF NOT EXISTS truewealth_positions_no_delete
+        BEFORE DELETE ON truewealth_snapshot_positions
+        BEGIN SELECT RAISE(ABORT, 'truewealth positions cannot be deleted'); END;
+        CREATE TRIGGER IF NOT EXISTS truewealth_cash_immutable_update
+        BEFORE UPDATE ON truewealth_snapshot_cash
+        BEGIN SELECT RAISE(ABORT, 'truewealth cash is immutable'); END;
+        CREATE TRIGGER IF NOT EXISTS truewealth_cash_no_delete
+        BEFORE DELETE ON truewealth_snapshot_cash
+        BEGIN SELECT RAISE(ABORT, 'truewealth cash cannot be deleted'); END;
+        CREATE TRIGGER IF NOT EXISTS truewealth_audit_immutable_update
+        BEFORE UPDATE ON audit_log WHEN OLD.entity_type='truewealth_import_batch'
+        BEGIN SELECT RAISE(ABORT, 'truewealth import audit is immutable'); END;
+        CREATE TRIGGER IF NOT EXISTS truewealth_audit_no_delete
+        BEFORE DELETE ON audit_log WHEN OLD.entity_type='truewealth_import_batch'
+        BEGIN SELECT RAISE(ABORT, 'truewealth import audit cannot be deleted'); END;
+        """
+    )
+
+
 def _apply_compat_migrations(conn: Connection) -> None:
     _add_missing_instrument_columns(conn)
     for table, text_columns in TEXT_AFFINITY_COLUMNS.items():
@@ -1955,6 +2122,7 @@ def _apply_compat_migrations(conn: Connection) -> None:
     _create_portfolio_ingestion_reconciliation_tables(conn)
     _create_daily_market_analytics_tables(conn)
     _create_postfinance_baseline_mapping_audit_v1(conn)
+    _create_truewealth_verified_snapshot_v1(conn)
     _create_grocery_optimizer_v1_tables(conn)
     _add_grocery_price_provider_v1_columns(conn)
     _create_grocery_matching_learning_v2_tables(conn)
diff --git a/tests/unit/test_budget_monthly_import_rule_learning_v1.py b/tests/unit/test_budget_monthly_import_rule_learning_v1.py
index 4c62f8e..2df4e53 100644
--- a/tests/unit/test_budget_monthly_import_rule_learning_v1.py
+++ b/tests/unit/test_budget_monthly_import_rule_learning_v1.py
@@ -109,6 +109,6 @@ def test_rule_learning_suggests_rule_from_manual_category_change_and_applies_wit

 def test_schema_version_32_grocery_optimizer_tables_exist() -> None:
     conn = db()
-    assert get_schema_version(conn) == 43
+    assert get_schema_version(conn) == 44
     for table in ["budget_import_sessions", "budget_rule_suggestions"]:
         assert conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table,)).fetchone()
diff --git a/tests/unit/test_budget_phase1.py b/tests/unit/test_budget_phase1.py
index 5facc71..4267bfd 100644
--- a/tests/unit/test_budget_phase1.py
+++ b/tests/unit/test_budget_phase1.py
@@ -29,7 +29,7 @@ def db():
 def test_budget_phase1_tables_seeds_and_text_decimal_columns_are_created_idempotently() -> None:
     conn = db()
     apply_migrations(conn)
-    assert get_schema_version(conn) == 43
+    assert get_schema_version(conn) == 44
     tables = {row["name"] for row in conn.execute("SELECT name FROM sqlite_master WHERE type='table'")}
     assert {
         "budget_accounts",
diff --git a/tests/unit/test_budget_phase11.py b/tests/unit/test_budget_phase11.py
index aa22f61..7ba16c9 100644
--- a/tests/unit/test_budget_phase11.py
+++ b/tests/unit/test_budget_phase11.py
@@ -19,7 +19,7 @@ def db():

 def test_budget_phase11_tables_tags_and_plan_items_are_created() -> None:
     conn = db()
-    assert get_schema_version(conn) == 43
+    assert get_schema_version(conn) == 44
     tables = {row["name"] for row in conn.execute("SELECT name FROM sqlite_master WHERE type='table'")}
     assert {"budget_plan_items", "budget_excel_seed_dry_runs", "budget_excel_seed_candidates"}.issubset(tables)
     cols = {row["name"]: row["type"] for row in conn.execute("PRAGMA table_info(budget_plan_items)")}
diff --git a/tests/unit/test_budget_phase12_seed_review.py b/tests/unit/test_budget_phase12_seed_review.py
index 551b454..40ff35a 100644
--- a/tests/unit/test_budget_phase12_seed_review.py
+++ b/tests/unit/test_budget_phase12_seed_review.py
@@ -35,7 +35,7 @@ def synthetic_workbook() -> dict[str, list[list[str]]]:

 def test_phase12_schema_has_requested_budget_seed_candidates_table() -> None:
     conn = db()
-    assert get_schema_version(conn) == 43
+    assert get_schema_version(conn) == 44
     cols = {row["name"]: row["type"] for row in conn.execute("PRAGMA table_info(budget_seed_candidates)")}
     assert {
         "seed_candidate_id",
diff --git a/tests/unit/test_budget_phase16_user_rules.py b/tests/unit/test_budget_phase16_user_rules.py
index a506448..58b0c83 100644
--- a/tests/unit/test_budget_phase16_user_rules.py
+++ b/tests/unit/test_budget_phase16_user_rules.py
@@ -49,7 +49,7 @@ def candidate(conn, description: str):

 def test_phase16_schema_adds_rule_name_and_applies_card_category_rules() -> None:
     conn = db()
-    assert get_schema_version(conn) == 43
+    assert get_schema_version(conn) == 44
     cats = add_categories(conn)
     seed_credit_card_candidates_from_rows(
         conn,
diff --git a/tests/unit/test_fixed_costs_subscriptions_v1.py b/tests/unit/test_fixed_costs_subscriptions_v1.py
index 63b57ef..3917421 100644
--- a/tests/unit/test_fixed_costs_subscriptions_v1.py
+++ b/tests/unit/test_fixed_costs_subscriptions_v1.py
@@ -48,7 +48,7 @@ def candidate(conn, category_id, merchant, amount, day, status="pending", classi

 def test_schema_38_creates_decimal_text_recurring_table_with_user_facing_fields() -> None:
     conn = db()
-    assert get_schema_version(conn) == 43
+    assert get_schema_version(conn) == 44
     cols = {r["name"]: r["type"] for r in conn.execute("PRAGMA table_info(budget_recurring_payments)").fetchall()}
     assert cols["expected_amount_text"].upper() == "TEXT"
     assert cols["amount_tolerance_pct"].upper() == "TEXT"
diff --git a/tests/unit/test_grocery_matching_learning_v2.py b/tests/unit/test_grocery_matching_learning_v2.py
index 5c44521..3594fb1 100644
--- a/tests/unit/test_grocery_matching_learning_v2.py
+++ b/tests/unit/test_grocery_matching_learning_v2.py
@@ -27,7 +27,7 @@ def db() -> sqlite3.Connection:
     conn = sqlite3.connect(':memory:')
     conn.row_factory = sqlite3.Row
     apply_migrations(conn)
-    assert get_schema_version(conn) == 43
+    assert get_schema_version(conn) == 44
     return conn

diff --git a/tests/unit/test_grocery_price_providers_v1.py b/tests/unit/test_grocery_price_providers_v1.py
index dc79241..e1141d3 100644
--- a/tests/unit/test_grocery_price_providers_v1.py
+++ b/tests/unit/test_grocery_price_providers_v1.py
@@ -29,7 +29,7 @@ def db() -> sqlite3.Connection:

 def test_schema_version_33_provider_cache_columns_exist() -> None:
     conn = db()
-    assert get_schema_version(conn) == 43
+    assert get_schema_version(conn) == 44
     cols = {row['name'] for row in conn.execute('PRAGMA table_info(grocery_product_details_cache)').fetchall()}
     assert {'brand', 'image_url', 'price_decimal_text', 'currency', 'unit', 'unit_price_decimal_text', 'availability_status', 'promotion_text', 'source', 'confidence', 'quality_flags_json', 'raw_result_json'}.issubset(cols)

diff --git a/tests/unit/test_portfolio_data_ingestion_reconciliation.py b/tests/unit/test_portfolio_data_ingestion_reconciliation.py
index 4837566..28a5ca5 100644
--- a/tests/unit/test_portfolio_data_ingestion_reconciliation.py
+++ b/tests/unit/test_portfolio_data_ingestion_reconciliation.py
@@ -115,8 +115,8 @@ def test_migrations_41_to_43_are_additive_and_ingestion_history_is_immutable(
     tmp_path: Path, monkeypatch: pytest.MonkeyPatch
 ) -> None:
     conn = database()
-    assert MIGRATION_VERSION == 43
-    assert conn.execute("SELECT MAX(version) FROM schema_migrations").fetchone()[0] == 43
+    assert MIGRATION_VERSION == 44
+    assert conn.execute("SELECT MAX(version) FROM schema_migrations").fetchone()[0] == 44
     assert {row[1] for row in conn.execute("PRAGMA table_info(instrument_price_mappings)")} >= {
         "source_symbol", "source_venue", "source_currency",
     }
diff --git a/tests/unit/test_schema.py b/tests/unit/test_schema.py
index 76683ed..f899370 100644
--- a/tests/unit/test_schema.py
+++ b/tests/unit/test_schema.py
@@ -15,7 +15,7 @@ def test_db_schema_can_be_created() -> None:
 def test_schema_version_recorded() -> None:
     conn = connect_memory()
     apply_migrations(conn)
-    assert get_schema_version(conn) == 43
+    assert get_schema_version(conn) == 44

 def test_decimal_sensitive_columns_use_text_affinity() -> None:


sed -n '1,260p' frontend/src/pages/TrueWealthPage.vue && printf '\n--- api ts ---\n' && sed -n '1,240p' frontend/src/api/truewealth.ts


Output:
Chunk ID: 551581
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 5420
Output:
<template>

    <LoadActionBar
      testid="refresh-truewealth"
      title="TrueWealth · Freie Anlagen"
      description="Offizielle Portfolio-Snapshots und provisorische manuelle Gesamtwerte mit klar getrenntem Datenstand."
      :loading="loading"
      @load="loadData(true)"
    />
    <LocalDataStatus :backend-ok="backendOk" :runtime-ok="runtimeOk" :loading="loading" :error="error" :last-loaded-at="lastLoadedAt" />

Freie Anlagen

Aktueller TrueWealth-Gesamtwert CHF

{{ summary?.current_label ?? 'Noch kein Wert verfügbar' }}

        <MoneyValue :value="summary?.current_value_chf ?? '0.00'" />

        Manuell / provisorisch. {{ summary.positions_label }}. Die Differenz wird weder Positionen noch Cash zugerechnet.

        Gesamtwert und Positionsdetails haben unterschiedliche Stichtage. Allokation, Gewinn und Rendite sind für diesen Mischstand deaktiviert.

        <button data-testid="truewealth-update-button" type="button" class="rounded-lg border border-cyan-300 bg-cyan-50 px-3 py-2 font-semibold text-cyan-800" @click="showManualForm = !showManualForm">Gesamtwert manuell aktualisieren</button>
        <button data-testid="truewealth-import-button" type="button" class="rounded-lg border border-slate-300 bg-white px-3 py-2 font-semibold text-slate-800" @click="showImportForm = !showImportForm">Offiziellen Auszug importieren</button>
        Zur Portfolio-Übersicht

Gesamtwert manuell aktualisieren

Erstellt einen neuen historischen, provisorischen Datensatz. Offizielle Positionen bleiben unverändert.

        Gesamtwert CHF<input data-testid="truewealth-amount-input" v-model="manualForm.total_value_chf" class="mt-1 w-full rounded-lg border border-slate-300 bg-white p-2" inputmode="decimal" placeholder="Betrag in CHF" @input="manualPreview = null" />
        Bewertungszeitpunkt<input data-testid="truewealth-date-input" v-model="manualForm.valuation_at" class="mt-1 w-full rounded-lg border border-slate-300 bg-white p-2" type="datetime-local" @input="manualPreview = null" />
        Notiz optional<input data-testid="truewealth-note-input" v-model="manualForm.note" class="mt-1 w-full rounded-lg border border-slate-300 bg-white p-2" maxlength="240" placeholder="z. B. Stand im Kundenportal" @input="manualPreview = null" />

        <button data-testid="truewealth-preview-button" class="rounded-lg bg-slate-900 px-3 py-2 font-semibold text-white disabled:opacity-50" :disabled="actionLoading || !manualForm.total_value_chf || !manualForm.valuation_at" @click="doManualPreview">Preview</button>
        <button v-if="manualPreview" data-testid="truewealth-confirm-button" class="rounded-lg bg-emerald-600 px-3 py-2 font-semibold text-white disabled:opacity-50" :disabled="actionLoading" @click="doManualConfirm">Confirm</button>

Preview: CHF {{ money(manualPreview.total_value_chf) }} · {{ formatDateTime(manualPreview.valuation_at) }}

Manuell / provisorisch. Positionsdetails werden nicht verändert.

Offiziellen TrueWealth-Auszug importieren

Nur unveränderte TrueWealth-Steuerauszüge als PDF. Der Import wird zuerst vollständig geprüft und angezeigt.

      <input data-testid="truewealth-file-input" class="mt-3 block w-full rounded-lg border border-slate-300 bg-white p-2 text-sm" type="file" accept="application/pdf,.pdf" @change="onFileSelected" />

        <button data-testid="truewealth-import-preview-button" class="rounded-lg bg-slate-900 px-3 py-2 font-semibold text-white disabled:opacity-50" :disabled="actionLoading || !importPayload" @click="doImportPreview">Import-Preview</button>
        <button v-if="importPreview" data-testid="truewealth-import-confirm-button" class="rounded-lg bg-emerald-600 px-3 py-2 font-semibold text-white disabled:opacity-50" :disabled="actionLoading || importPreview.reconciliation_status === 'mismatch' || importPreview.conflict" @click="doImportConfirm">Import bestätigen</button>

Stichtag

{{ formatDate(importPreview.snapshot_date) }}

Gesamtwert

CHF {{ money(importPreview.source_total_chf) }}

Inhalt

{{ importPreview.positions.length }} Positionen · {{ importPreview.cash.length }} Cashkonten

Reconciliation

{{ importPreview.reconciliation_status }} · CHF {{ money(importPreview.reconciliation_difference_chf) }}

Identische Quelle bereits bestätigt. Ein erneuter Confirm ist idempotent.

Konflikt: Für diesen Stichtag existiert bereits eine andere offizielle Quelle. Confirm ist gesperrt.

{{ message }}

{{ actionError }}

Positionsdetails

{{ summary?.positions_label }}

{{ summary?.positions.length ?? 0 }} Positionen

{{ position.instrument_name }}

{{ position.isin }}

Menge
{{ position.quantity }}Quellwert
CHF {{ money(position.source_value_chf) }}

Noch keine offiziellen Positionen importiert.

{{ cash.currency }} {{ cash.amount_original }} · CHF {{ money(cash.source_value_chf) }}

Bewertungshistorie

Manuelle und offizielle Werte bleiben historisch und auditierbar erhalten.

CHF {{ money(item.total_value_chf) }}

{{ formatDateTime(item.valuation_at) }}

{{ sourceLabel(item.source_type) }}

{{ item.notes }}

Deaktiviert · historisch erhalten

          <button v-if="item.source_type === 'truewealth_manual_provisional'" class="mt-3 rounded-lg border border-slate-300 px-2 py-1 text-xs font-semibold" :data-testid="`truewealth-history-status-${item.snapshot_id}`" @click="toggleManualStatus(item)">{{ item.is_active ? 'Provisorischen Wert deaktivieren' : 'Wert reaktivieren' }}</button>

Importhistorie

Offizieller Snapshot {{ formatDate(item.snapshot_date) }}

{{ item.position_count }} Positionen · {{ item.cash_count }} Cashkonten · CHF {{ money(item.source_total_chf) }}

Parser {{ item.parser_id }} v{{ item.parser_version }} · {{ item.reconciliation_status }}

Noch keine bestätigten offiziellen Importe.

</template>

--- api ts ---
import { apiGet, apiPost } from './client'

export type TrueWealthPosition = {
  snapshot_position_id: string
  instrument_name: string
  isin: string
  asset_type: string | null
  quantity: string
  price_currency: string
  source_price: string
  source_value_chf: string
}

export type TrueWealthCash = {
  snapshot_cash_id: string
  currency: string
  amount_original: string
  fx_rate_to_chf: string | null
  source_value_chf: string
}

export type TrueWealthHistory = {
  snapshot_id: string
  total_value_chf: string
  valuation_date: string
  valuation_at: string
  source_type: string
  quality_status: string
  notes: string | null
  is_active: number
  deactivated_at: string | null
  deactivation_reason: string | null
}

export type TrueWealthImportHistory = {
  batch_id: string
  snapshot_id: string
  snapshot_date: string
  parser_id: string
  parser_version: string
  confirmed_at: string
  status: string
  position_count: number
  cash_count: number
  source_total_chf: string
  securities_total_chf: string
  cash_total_chf: string
  components_total_chf: string
  reconciliation_difference_chf: string
  reconciliation_status: string
  completeness_status: string
}

export type TrueWealthSummary = {
  portfolio_label: string
  current_value_chf: string | null
  current_valuation_at: string | null
  current_source_type: string
  current_is_manual_provisional: boolean
  current_label: string
  positions_snapshot_date: string | null
  positions_label: string
  mixed_as_of: boolean
  allocation_performance_usable: boolean
  positions: TrueWealthPosition[]
  cash: TrueWealthCash[]
  history: TrueWealthHistory[]
  imports: TrueWealthImportHistory[]
  latest_official: Record<string, unknown> | null
}

export type TrueWealthImportPayload = {
  file_name: string
  content_base64: string
}

export type TrueWealthImportPreview = TrueWealthImportPayload & {
  preview_id: string
  confirmation_id: string
  portfolio_label: string
  snapshot_date: string
  period_from: string
  period_to: string
  file_sha256: string
  page_count: number
  source_total_chf: string
  securities_total_chf: string
  cash_total_chf: string
  components_total_chf: string
  reconciliation_difference_chf: string
  reconciliation_status: string
  positions: Array<{ name: string; isin: string; quantity: string; currency: string; source_price: string; source_value_chf: string }>
  cash: Array<{ currency: string; amount_original: string; fx_rate_to_chf: string | null; source_value_chf: string }>
  duplicate: boolean
  existing_batch_id: string | null
  conflict: boolean
  conflicting_batch_id: string | null
  warnings: string[]
}

export type ManualValuePayload = { total_value_chf: string; valuation_at: string; note: string }
export type ManualValuePreview = ManualValuePayload & {
  preview_id: string
  confirmation_id: string
  valuation_date: string
  source_type: string
  status: string
  warnings: string[]
}

export const getTrueWealthSummary = (refresh = false) => apiGet<TrueWealthSummary>('/api/truewealth/summary', { refresh })
export const previewTrueWealthImport = (payload: TrueWealthImportPayload) => apiPost<TrueWealthImportPreview>('/api/truewealth/imports/preview', payload)
export const confirmTrueWealthImport = (payload: TrueWealthImportPayload & Pick<TrueWealthImportPreview, 'preview_id' | 'confirmation_id'> & { confirm: true }) => apiPost<{ status: string; batch_id: string; snapshot_id?: string; audit_id: string; idempotent: boolean }>('/api/truewealth/imports/confirm', payload)
export const previewManualTrueWealthValue = (payload: ManualValuePayload) => apiPost<ManualValuePreview>('/api/truewealth/manual-values/preview', payload)
export const confirmManualTrueWealthValue = (payload: ManualValuePayload & Pick<ManualValuePreview, 'preview_id' | 'confirmation_id'> & { confirm: true }) => apiPost<{ status: string; snapshot_id: string; audit_id: string }>('/api/truewealth/manual-values/confirm', payload)
export const setManualTrueWealthValueStatus = (snapshotId: string, active: boolean, note: string) => apiPost<{ status: string; snapshot_id: string; audit_id: string; idempotent: boolean }>(`/api/truewealth/manual-values/${encodeURIComponent(snapshotId)}/status`, { active, note, confirm: true })


sed -n '1,280p' src/jarvis_finance/api/routers/truewealth.py && printf '\n--- schemas ---\n' && sed -n '1,260p' src/jarvis_finance/api/schemas/truewealth.py && printf '\n--- service 1 ---\n' && sed -n '1,360p' src/jarvis_finance/services/truewealth_service.py


Output:
Chunk ID: b0c477
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 4624
Output:
from __future__ import annotations

from fastapi import APIRouter, Depends, HTTPException

from jarvis_finance.api.dependencies import get_db
from jarvis_finance.api.schemas.truewealth import (
    TrueWealthImportConfirmRequest,
    TrueWealthImportPreviewRequest,
    TrueWealthManualConfirmRequest,
    TrueWealthManualPreviewRequest,
    TrueWealthManualStatusRequest,
)
from jarvis_finance.services.truewealth_service import (
    confirm_manual_truewealth_value,
    confirm_truewealth_import,
    get_truewealth_summary,
    preview_manual_truewealth_value,
    preview_truewealth_import,
    set_manual_truewealth_value_active,
)

router = APIRouter(prefix="/truewealth", tags=["truewealth"])

def _run(callable_, *args):
    try:
        return callable_(*args)
    except ValueError as exc:
        raise HTTPException(status_code=422, detail=str(exc)) from exc

@router.get("/summary")
def summary(conn=Depends(get_db)):
    return _run(get_truewealth_summary, conn)

@router.post("/imports/preview")
def import_preview(request: TrueWealthImportPreviewRequest, conn=Depends(get_db)):
    return _run(preview_truewealth_import, conn, request.model_dump())

@router.post("/imports/confirm")
def import_confirm(request: TrueWealthImportConfirmRequest, conn=Depends(get_db)):
    return _run(confirm_truewealth_import, conn, request.model_dump())

@router.post("/manual-values/preview")
def manual_preview(request: TrueWealthManualPreviewRequest, conn=Depends(get_db)):
    return _run(preview_manual_truewealth_value, conn, request.model_dump())

@router.post("/manual-values/confirm")
def manual_confirm(request: TrueWealthManualConfirmRequest, conn=Depends(get_db)):
    return _run(confirm_manual_truewealth_value, conn, request.model_dump())

@router.post("/manual-values/{snapshot_id}/status")
def manual_status(
    snapshot_id: str,
    request: TrueWealthManualStatusRequest,
    conn=Depends(get_db),
):
    return _run(set_manual_truewealth_value_active, conn, snapshot_id, request.model_dump())

--- schemas ---
from __future__ import annotations

from pydantic import BaseModel, ConfigDict, Field

class TrueWealthImportPreviewRequest(BaseModel):
    model_config = ConfigDict(extra="forbid")
    account_id: str | None = Field(default=None, min_length=1, max_length=120)
    file_name: str = Field(min_length=1, max_length=200)
    content_base64: str = Field(min_length=8, max_length=8_000_000)

class TrueWealthImportConfirmRequest(TrueWealthImportPreviewRequest):
    preview_id: str = Field(min_length=10, max_length=80)
    confirmation_id: str = Field(min_length=10, max_length=80)
    confirm: bool

class TrueWealthManualPreviewRequest(BaseModel):
    model_config = ConfigDict(extra="forbid")
    total_value_chf: str = Field(min_length=1, max_length=40)
    valuation_at: str = Field(min_length=10, max_length=40)
    note: str = Field(default="", max_length=240)

class TrueWealthManualConfirmRequest(TrueWealthManualPreviewRequest):
    preview_id: str = Field(min_length=10, max_length=80)
    confirmation_id: str = Field(min_length=10, max_length=80)
    confirm: bool

class TrueWealthManualStatusRequest(BaseModel):
    model_config = ConfigDict(extra="forbid")
    active: bool
    note: str = Field(default="", max_length=240)
    confirm: bool

--- service 1 ---
from __future__ import annotations

import base64
import binascii
from datetime import UTC, datetime
from decimal import Decimal, InvalidOperation
from hashlib import sha256
import json
import os
from pathlib import Path
from sqlite3 import Connection, IntegrityError
from typing import Any
from uuid import uuid4

from jarvis_finance.config.paths import DEFAULT_RUNTIME_DIR
from jarvis_finance.imports.truewealth_tax_statement import (
    PARSER_ID,
    PARSER_VERSION,
    TrueWealthStatement,
    parse_truewealth_tax_statement,
)

TRUEWEALTH_PORTFOLIO_ID = "truewealth-free-assets"
TRUEWEALTH_LABEL = "Freie Anlagen"

def _now() -> str:
    return datetime.now(UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z")

def _id(prefix: str) -> str:
    return f"{prefix}_{uuid4().hex[:20]}"

def _json_hash(value: Any) -> str:
    return sha256(json.dumps(value, sort_keys=True, separators=(",", ":"), default=str).encode()).hexdigest()

def _decimal_text(value: Decimal, places: str = "0.01") -> str:
    return format(value.quantize(Decimal(places)), "f")

def _decode_pdf(content_base64: str) -> bytes:
    try:
        return base64.b64decode(content_base64, validate=True)
    except (binascii.Error, ValueError) as exc:
        raise ValueError("TrueWealth PDF is not valid base64") from exc

def _resolve_account_id(conn: Connection) -> str:
    mapped = conn.execute(
        "SELECT account_id FROM truewealth_portfolios WHERE portfolio_id=? AND is_active=1",
        (TRUEWEALTH_PORTFOLIO_ID,),
    ).fetchone()
    if mapped:
        return str(mapped["account_id"])
    configured = os.environ.get("JARVIS_TRUEWEALTH_ACCOUNT_ID", "").strip()
    if configured:
        return configured
    candidates = conn.execute(
        """SELECT DISTINCT a.account_id
           FROM accounts a
           JOIN platforms p ON p.platform_id=a.platform_id
           WHERE a.is_active=1
             AND lower(p.name) LIKE '%true%wealth%'
             AND EXISTS (
                 SELECT 1 FROM account_value_snapshots v
                 WHERE v.account_id=a.account_id AND COALESCE(v.is_active,1)=1
             )
           ORDER BY a.account_id"""
    ).fetchall()
    if len(candidates) != 1:
        raise ValueError("Confirmed TrueWealth target account cannot be resolved unambiguously")
    return str(candidates[0]["account_id"])

def _validate_account(conn: Connection, account_id: str | None = None) -> str:
    resolved = _resolve_account_id(conn)
    if account_id and account_id != resolved:
        raise ValueError("TrueWealth portfolio account does not match the confirmed mapping")
    row = conn.execute(
        """SELECT a.account_id,a.is_active,p.name AS platform
           FROM accounts a JOIN platforms p ON p.platform_id=a.platform_id
           WHERE a.account_id=?""",
        (resolved,),
    ).fetchone()
    if not row or not row["is_active"] or "true wealth" not in str(row["platform"]).lower():
        raise ValueError("Confirmed TrueWealth target account is unavailable")
    return resolved

def _revision(conn: Connection, account_id: str) -> str:
    row = conn.execute(
        """SELECT COALESCE(MAX(created_at),'') AS latest,COUNT(*) AS count
           FROM account_value_snapshots WHERE account_id=?""",
        (account_id,),
    ).fetchone()
    imported = conn.execute("SELECT COUNT(*) FROM truewealth_import_batches").fetchone()[0]
    return _json_hash([row["latest"], row["count"], imported])

def _preview_identity(kind: str, payload: dict[str, Any], revision: str) -> tuple[str, str]:
    fingerprint = _json_hash([kind, payload, revision])
    return f"twpreview_{fingerprint[:24]}", f"twconfirm_{fingerprint[24:48]}"

def _statement_preview_payload(statement: TrueWealthStatement, filename: str) -> dict[str, Any]:
    difference = statement.source_total_chf - statement.components_total_chf
    return {
        "portfolio_label": TRUEWEALTH_LABEL,
        "snapshot_date": statement.statement_date,
        "period_from": statement.period_from,
        "period_to": statement.period_to,
        "file_sha256": statement.file_sha256,
        "filename_sha256": sha256(filename.encode()).hexdigest(),
        "page_count": statement.page_count,
        "source_total_chf": _decimal_text(statement.source_total_chf),
        "securities_total_chf": _decimal_text(statement.securities_total_chf),
        "cash_total_chf": _decimal_text(statement.cash_total_chf),
        "components_total_chf": _decimal_text(statement.components_total_chf),
        "reconciliation_difference_chf": _decimal_text(difference),
        "reconciliation_status": "matched" if difference == 0 else "within_tolerance",
        "positions": [
            {
                "name": row.name,
                "isin": row.isin,
                "quantity": format(row.quantity, "f"),
                "currency": row.currency,
                "source_price": format(row.source_price, "f"),
                "source_value_chf": _decimal_text(row.source_value_chf),
            }
            for row in statement.positions
        ],
        "cash": [
            {
                "currency": row.currency,
                "amount_original": format(row.amount_original, "f"),
                "fx_rate_to_chf": format(row.fx_rate_to_chf, "f") if row.fx_rate_to_chf is not None else None,
                "source_value_chf": _decimal_text(row.value_chf),
            }
            for row in statement.cash
        ],
        "parser_id": PARSER_ID,
        "parser_version": PARSER_VERSION,
    }

def preview_truewealth_import(conn: Connection, request: dict[str, Any]) -> dict[str, Any]:
    requested = str(request.get("account_id") or "").strip() or None
    account_id = _validate_account(conn, requested)
    raw = _decode_pdf(str(request.get("content_base64") or ""))
    filename = str(request.get("file_name") or "source.pdf")
    statement = parse_truewealth_tax_statement(raw)
    payload = _statement_preview_payload(statement, filename)
    preview_id, confirmation_id = _preview_identity("official_import", payload, _revision(conn, account_id))
    duplicate = conn.execute(
        "SELECT batch_id FROM truewealth_import_batches WHERE file_sha256=?",
        (statement.file_sha256,),
    ).fetchone()
    conflict = conn.execute(
        """SELECT batch_id FROM truewealth_import_batches
           WHERE account_id=? AND snapshot_date=? AND file_sha256<>? LIMIT 1""",
        (account_id, statement.statement_date, statement.file_sha256),
    ).fetchone()
    warnings = []
    if statement.source_total_chf != statement.components_total_chf:
        warnings.append("Source total is rounded to whole CHF; components differ within CHF 1.00")
    if conflict:
        warnings.append("A different official source already exists for this snapshot date")
    return {
        **payload,
        "preview_id": preview_id,
        "confirmation_id": confirmation_id,
        "duplicate": bool(duplicate),
        "existing_batch_id": str(duplicate["batch_id"]) if duplicate else None,
        "conflict": bool(conflict),
        "conflicting_batch_id": str(conflict["batch_id"]) if conflict else None,
        "warnings": warnings,
    }

def _archive_pdf(raw: bytes, digest: str) -> tuple[str, bool]:
    root = Path(os.environ.get("JARVIS_FINANCE_RUNTIME_DIR", str(DEFAULT_RUNTIME_DIR))).expanduser()
    folder = root / "imports" / "truewealth" / "archive"
    folder.mkdir(mode=0o700, parents=True, exist_ok=True)
    try:
        folder.chmod(0o700)
    except OSError:
        pass
    target = folder / f"{digest}.pdf"
    created = False
    if target.exists():
        if sha256(target.read_bytes()).hexdigest() != digest:
            raise ValueError("Archived TrueWealth source hash mismatch")
    else:
        tmp = folder / f".{digest}.{uuid4().hex}.tmp"
        try:
            fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
            with os.fdopen(fd, "wb") as handle:
                handle.write(raw)
                handle.flush()
                os.fsync(handle.fileno())
            os.replace(tmp, target)
            target.chmod(0o600)
            created = True
        finally:
            if tmp.exists():
                tmp.unlink()
    return f"truewealth/archive/{digest}.pdf", created

def _remove_new_archive(reference: str) -> None:
    root = Path(os.environ.get("JARVIS_FINANCE_RUNTIME_DIR", str(DEFAULT_RUNTIME_DIR))).expanduser()
    target = root / "imports" / reference
    try:
        target.unlink(missing_ok=True)
    except OSError:
        pass

def _insert_audit(
    conn: Connection,
    *,
    action: str,
    entity_type: str,
    entity_id: str,
    details: dict[str, Any],
    note: str = "",
    quality: str = "ok",
) -> str:
    audit_id = _id("audit")
    now = _now()
    conn.execute(
        """INSERT INTO audit_log(
             audit_id,timestamp,source,action,entity_type,entity_id,old_values_json,new_values_json,
             user_text_note,confirmed,confirmation_timestamp,auto_parsed,parse_confidence,created_by,
             quality_status,created_at)
           VALUES(?,?,'truewealth_sprint12',?,?,?,NULL,?,?,1,?,0,NULL,'user',?,?)""",
        (audit_id, now, action, entity_type, entity_id, json.dumps(details, sort_keys=True), note, now, quality, now),
    )
    return audit_id

def confirm_truewealth_import(conn: Connection, request: dict[str, Any]) -> dict[str, Any]:
    if request.get("confirm") is not True:
        raise ValueError("Explicit TrueWealth import confirmation is required")
    preview = preview_truewealth_import(conn, request)
    if request.get("preview_id") != preview["preview_id"] or request.get("confirmation_id") != preview["confirmation_id"]:
        raise ValueError("TrueWealth import preview is stale or changed")
    if preview["duplicate"]:
        row = conn.execute(
            "SELECT batch_id,audit_id FROM truewealth_import_batches WHERE file_sha256=?",
            (preview["file_sha256"],),
        ).fetchone()
        return {"status": "confirmed", "batch_id": row["batch_id"], "audit_id": row["audit_id"], "idempotent": True}
    if preview["conflict"]:
        raise ValueError("A different official TrueWealth source already exists for this snapshot date")
    account_id = _validate_account(conn, str(request.get("account_id") or "").strip() or None)

    raw = _decode_pdf(str(request.get("content_base64") or ""))
    statement = parse_truewealth_tax_statement(raw)
    archive_reference, archive_created = _archive_pdf(raw, statement.file_sha256)
    now = _now()
    batch_id = _id("twbatch")
    snapshot_id = _id("twsnapshot")
    account_snapshot_id = _id("acctval")
    difference = statement.source_total_chf - statement.components_total_chf
    try:
        conn.execute("BEGIN IMMEDIATE")
        _validate_account(conn, account_id)
        existing_portfolios = conn.execute(
            "SELECT portfolio_id,account_id FROM truewealth_portfolios WHERE is_active=1"
        ).fetchall()
        if existing_portfolios and any(
            row["portfolio_id"] != TRUEWEALTH_PORTFOLIO_ID or row["account_id"] != account_id
            for row in existing_portfolios
        ):
            raise ValueError("TrueWealth portfolio mapping is ambiguous")
        conn.execute(
            """INSERT OR IGNORE INTO truewealth_portfolios(
                 portfolio_id,account_id,source_reference_hash,label,portfolio_kind,base_currency,is_active,created_at)
               VALUES(?,?,?,'Freie Anlagen','free_assets','CHF',1,?)""",
            (TRUEWEALTH_PORTFOLIO_ID, account_id, _json_hash(["TrueWealth", "Freie Anlagen"]), now),
        )
        audit_id = _insert_audit(
            conn,
            action="truewealth_import_confirm",
            entity_type="truewealth_import_batch",
            entity_id=batch_id,
            details={
                "snapshot_date": statement.statement_date,
                "file_sha256": statement.file_sha256,
                "position_count": len(statement.positions),
                "cash_count": len(statement.cash),
                "parser_id": PARSER_ID,
                "parser_version": PARSER_VERSION,
            },
        )
        conn.execute(
            """INSERT INTO truewealth_import_batches(
                 batch_id,portfolio_id,account_id,file_sha256,filename_sha256,source_file_type,parser_id,
                 parser_version,provenance,snapshot_date,page_count,archive_reference,status,audit_id,confirmed_at,confirmed_by)
               VALUES(?,?,?,?,?,'application/pdf',?,?,'truewealth_customer_export',?,?,?,'confirmed',?,?,'user')""",
            (
                batch_id,
                TRUEWEALTH_PORTFOLIO_ID,
                account_id,
                statement.file_sha256,
                preview["filename_sha256"],
                PARSER_ID,
                PARSER_VERSION,
                statement.statement_date,
                statement.page_count,
                archive_reference,
                audit_id,
                now,
            ),
        )
        conn.execute(
            """INSERT INTO truewealth_snapshots(
                 snapshot_id,portfolio_id,account_id,batch_id,snapshot_date,source_total_chf,securities_total_chf,
                 cash_total_chf,components_total_chf,reconciliation_difference_chf,reconciliation_tolerance_chf,
                 reconciliation_status,position_count,cash_count,completeness_status,reason_codes_json,created_at)
               VALUES(?,?,?,?,?,?,?,?,?,?,'1.00',?,?,?,'complete',?,?)""",
            (
                snapshot_id,
                TRUEWEALTH_PORTFOLIO_ID,
                account_id,
                batch_id,
                statement.statement_date,
                _decimal_text(statement.source_total_chf),
                _decimal_text(statement.securities_total_chf),
                _decimal_text(statement.cash_total_chf),
                _decimal_text(statement.components_total_chf),
                _decimal_text(difference),
                "matched" if difference == 0 else "within_tolerance",
                len(statement.positions),
                len(statement.cash),
                json.dumps(["source_total_rounded"] if difference else []),
                now,
            ),
        )
        for row in statement.positions:
            conn.execute(
                """INSERT INTO truewealth_snapshot_positions(
                     snapshot_position_id,snapshot_id,source_row_reference,instrument_name,isin,asset_type,quantity,
                     price_currency,source_price,source_value_chf,source_evidence_json,created_at)
                   VALUES(?,?,?,?,?,NULL,?,?,?,?,?,?)""",
                (
                    _id("twposition"),
                    snapshot_id,
                    row.source_ref,
                    row.name,
                    row.isin,
                    format(row.quantity, "f"),
                    row.currency,
                    format(row.source_price, "f"),
                    _decimal_text(row.source_value_chf),
                    json.dumps({"source": "official_tax_statement", "estimated": False}),
                    now,


sed -n '360,760p' src/jarvis_finance/services/truewealth_service.py && printf '\n--- parser ---\n' && sed -n '1,360p' src/jarvis_finance/imports/truewealth_tax_statement.py